Mutation not requesting for actively fetched container data in fatQuery on RANGE_ADD - relayjs

I am trying to do a RANGE_ADD mutation using what is now known as Relay Classic (this probably is resolved with Modern). I get the error:
Warning: writeRelayUpdatePayload(): Expected response payload to include the newly created edge `newThingEdge` and its `node` field. Did you forget to update the `RANGE_ADD` mutation config?
So, yes, the payload is not sending the anything more than the clientMutationId in the expected response shape because the request mutation is not asking for it.
According to #Joe Savona here, https://github.com/facebook/relay/issues/521, this might be happening if there is no intersecting container requesting for this data. But that's not entirely true for me. My Route is requesting:
things: (Component) => Relay.QL`
query {
allThings(variable: $variable) {
${Component.getFragment('things')},
}
}
`,
while my fatQuery is requesting for:
fragment on AddMockThing {
allThings(variable: "${variable}", first: 100) {
edges {
node {
id,
},
},
},
newThingEdge
}
Now you may say these aren't the same queries, because of the extra first: 100 in the getFatQuery version, but if I don't use that, I get the error:
Error: Error: You supplied the 'edges' field on a connection named 'allThings', but you did not supply an argument necessary to do so. Use either the 'find', 'first', or 'last' argument.
On the other hand, if I add first: 100 to the Route query, I get the error: Error: Invalid root field 'allThings'; Relay only supports root fields with zero or one argument.
Stuck between a fatQuery and a hard place. Would appreciate the help!

You're getting a validation error because the Relay compiler is looking for a connection argument (first: X). You can disable this particular validation by adding the #relay(pattern: true) directive. This marks the fat query as a ‘pattern’ to match against, rather than as something concrete.
fragment on AddMockThing #relay(pattern: true) {
allThings(variable: "${variable}") {
edges {
node {
id,
},
},
},
newThingEdge
}
More info here: https://stackoverflow.com/a/34112045/802047

Related

$batch request resulting in error "Default changeset implementation allows only one operation"

I am making a worklist application using SAPUI5. The problem is that when I create an entry and then create another one right after that, I get the following error:
Default changeset implementation allows only one operation.
I checked the $batch header and I see that there is a MERGE and a POST, with the MERGE updating the previous entry for some reason. Can anyone shed some light? Could it be a backend error and not a UI5 error?
Creating the new entry:
_onMetadataLoaded: function() {
var oModel = this.getView().getModel();
var that = this;
// ...
oModel.read("/USERS_SET", {
success: function(oData) {
var oProperties = {
Qmnum: "0",
Otherstuff: "cool"
};
that._oContext = that._oView.getModel().createEntry("/ENTITYSET", {
properties: oProperties
});
that.getView().setBindingContext(that._oContext);
// ...
}
});
},
handleSavePress: function(oEvent) {
// ...
this.getView().getModel().submitChanges({
success: function(oData) {
// ...
},
error: function(oError) {
// ...
}
});
},
tl-dr: Apparently you must be using the SAP Gateway. If you do not need to process those requests in one transaction then send them in different changesets. If you do not need batch calls at all consider turning it off by supplying your model with "useBatch": false upon instantiation. However if you need to process the requests together in one transaction then you have to read the details below.
In order to understand the problem you have to understand how the gateway and the batch and changeset requests work.
Batch requests consist of multiple requests bundled together. The purpose is to open only one connection and group together relevant requests so that the overhead is minimalized. Changesets form smaller blocks inside batch requests, where modification requests can be bundled and processed together in order to ensure an all-or-nothing characteristic.
So on the gateway side: there are two relevant classes for your OData service, assuming that you have used the SAP Gateway Service Builder (SEGW transaction). There is one with the ending ...DPC and one with ...DPC_EXT. Don't touch the former, it will be always regenerated when you update your service in the service builder. The latter is the one that we will need in this example. You will have to redefine at least two methods:
/IWBEP/IF_MGW_APPL_SRV_RUNTIME~CHANGESET_BEGIN
/IWBEP/IF_MGW_APPL_SRV_RUNTIME~CHANGESET_PROCESS
By default the changeset_begin method will only allow changeset processing for changesets where the number of requests equals to one. This can be handled automatically that's why a limitation exists. If there were more requests one could not ensure their processing automatically as they could have a business dependency on each other.
So make sure to allow a bundled (deferred mode) processing of changesets under the desired conditions:
/IWBEP/IF_MGW_APPL_SRV_RUNTIME~CHANGESET_BEGIN: first call the super->/iwbep/if_mgw_appl_srv_runtime~changeset_begin method in a try catch block, then loop at it_operation_info to decide and narrow down processing only in selected cases and then allow cv_defer_mode only for the selected cases, otherwise throw a /iwbep/cx_mgw_tech_exception=>changeset_not_supported exception.
/IWBEP/IF_MGW_APPL_SRV_RUNTIME~CHANGESET_PROCESS: all requests will be available in the it_changeset_request. Make sure to fill the ct_changeset_response table with the responses.
METHOD /iwbep/if_mgw_appl_srv_runtime~changeset_process.
DATA:
lv_operation_counter TYPE i VALUE 0,
lr_context TYPE REF TO /iwbep/cl_mgw_request,
lr_entry_provider TYPE REF TO /iwbep/if_mgw_entry_provider,
lr_message_container TYPE REF TO /iwbep/if_message_container,
lr_entity_data TYPE REF TO data,
ls_context_details TYPE /iwbep/if_mgw_core_srv_runtime=>ty_s_mgw_request_context,
ls_changeset_response LIKE LINE OF ct_changeset_response.
FIELD-SYMBOLS:
<fs_ls_changeset_request> LIKE LINE OF it_changeset_request.
LOOP AT it_changeset_request ASSIGNING <fs_ls_changeset_request>.
lr_context ?= <fs_ls_changeset_request>-request_context.
lr_entry_provider = <fs_ls_changeset_request>-entry_provider.
lr_message_container = <fs_ls_changeset_request>-msg_container.
ls_context_details = lr_context->get_request_details( ).
CASE ls_context_details-target_entity.
WHEN 'SomeEntity'.
"Do the processing here
WHEN OTHERS.
ENDCASE.
ENDLOOP.
ENDMETHOD.
From the error I can tell you must be using SAP GW :-) This happens only for batch requests containing more than one create/delete/update calls and it's related to transaction security ("all or nothing"). What you have to do is redefining the corresponding GW method, I think it was CHANGESET_BEGIN. See https://archive.sap.com/discussions/thread/3562720 for some details (can't offer more for now...).

How to write a security rule to allow read without using auth variable

I am trying to write Security rules, but I am bit confused on writing it. For my case I am not authenticating the users using Firebase, but I have node in database which has child named by usernames. I am trying to achieve logic like this: for any child of this node if value is true then he can move on further or else not. Here is my sample node
"Customers":{
"John":"true",
"Jack":"false"
}
"Messages":{
"Message1":{
....
},
},
And here is my rules node where I am confused.I have tried using "$" wild card variable but getting error that variable is unknown.
"rules":{
"Messages":{
".read":"root.child('Customers').child($name).val()===true",
".write":"root.child('Customers').child($name).val()===true"
}
}
I think the "$" variable can't be used this way. So how should I do this?
How do you decide which user to check for value? You must have a value for comparison. If you want a logic like users can see their own messages, you should add an Username field under messages node. Like;
"Messages":{
"Message1":{
"John": {
},
....
},
}
And with this field you can do this;
"rules":{
"Messages":{
"$userId" : {
".read":"root.child('Customers').child($userId).val()===true",
".write":"root.child('Customers').child($userId).val()===true"
}
}
}

Search with Relay doesn't include new results due to local cache

I've implemented a search-as-you-type component in React and Relay. It's roughly the same setup as search functionality using relay
It works as intended with one exception. New results from the server never appear when I retype a search I've already performed on the client. I looks like Relay always goes to the local cache in this case.
So, for example, say I've searched for 'foo' and didn't find any results. Now, seconds later, another user on the website creates this 'foo', but Relay will never query the server since the cached response to the 'foo' search was an empty result.
Is there a pattern or best practice for this scenario?
The query is as follows. I call this.props.relay.setVariables to perform the search:
initialVariables: {
search: '',
hasSearch: false
},
fragments: {
me: () => Relay.QL`
fragment on Viewer {
relationSearch(search: $search) #include(if: $hasSearch) {
... on User {
username
}
}
}
`
}
The answer seems to be to use this.props.relay.forceFetch with the search variables instead.
See https://facebook.github.io/relay/docs/api-reference-relay-container.html#forcefetch
Someone correct me if this isn't best practice.

Mutation not fetching data specified by the fat query in conjunction with RANGE_ADD?

I'm trying to understand RANGE_ADD. I've provided the mutation config, for RANGE_ADD, with all the required information. I use the viewer naming convention, with my connections nested within viewer. Below is what my complete Relay Mutation looks like ...
import Relay from "react-relay";
export default class CreateTeamMutation extends Relay.Mutation {
static fragments = {
viewer: () => Relay.QL`
fragment on Viewer {
id
}
`
}
getMutation() {
return Relay.QL`
mutation { createTeam }
`;
}
getVariables() {
return {
name: this.props.name
};
}
getFatQuery() {
return Relay.QL`
fragment on CreateTeamPayload {
edge,
viewer {
teams
}
}
`;
}
getConfigs() {
console.log(this.props.viewer);
return [{
type: "RANGE_ADD",
edgeName: "edge",
parentID: this.props.viewer.id,
parentName: "viewer",
connectionName: "teams",
rangeBehaviors: {
"": "append"
}
}];
}
};
I provide a fragment for the viewer id, which at run-time, within getConfigs, I can see is present.
In the GraphQL Mutation response payload, CreateTeamPayload, the viewer field is provided so that Relay can make use of the connection within the mutation config for RANGE_ADD. Also, within CreateTeamPayload, the new edge is provided as edge.
These three bits of info (viewer id for the parent, the connection information and the edge) seem to be all that RANGE_ADD demands. I also make sure to request this data from the server, via the fat query so that Relay has access to it for the mutation config.
Relay does not seem to be including what I've specified in the fat query, and what is required for the mutation config, in what it dispatches to the server, though. All that Relay is requesting is the clientMutationId. Here is the request made by Relay ...
{
"query": "mutation CreateTeamMutation($input_0:CreateTeamInput!){createTeam(input:$input_0){clientMutationId}}",
"variables": {
"input_0": {
"name": "foo bar",
"clientMutationId": "0"
}
}
}
And, in chain reaction fashion, this causes Relay, who is expecting the viewer and edge for the mutation config, to throw an error ...
Warning: writeRelayUpdatePayload(): Expected response payload to include the newly created edge `edge` and its `node` field. Did you forget to update the `RANGE_ADD` mutation config?
Those required fields could totally be there if Relay had included them. Does RANGE_ADD have to be accompanied by REQUIRED_CHILDREN for this to work? The mutation goes through to the server, and the record is created on the server, it's just the client-side mutation config fails to incorporate the changed data into the store.
This is probably a case of an ambiguous warning: https://github.com/facebook/relay/issues/542
Relay intersects the range behaviours with the previously fetched connections.
Your range behaviors here only define what to do when the teams connection is not under the influence of any call. How are you calling your teams connection in the app ?
For example if your app fetches teams(order_by: 'recent'), you should define a range behavior like this one 'orderby(recent)': 'append'

"Internal Error in key fixup - unable to locate entity" error but database updated as expected

I'm struggling to debug this error because, although it's consistently reported, the app's behavior is as intended. Would appreciate pointers as to what it means and how I could go about debugging its source.
Apologies for being vague but, since I'm getting the desired result, I'm unsure what other information to provide.
UPDATE
I've created a repro of this issue and attempted to focus the code on the problem. The error is thrown consistently even though the database is updated correctly. There's a single saveChanges in the code and it uses the save functionality from the dataservice.js in the Breeze Todo sample. SaveOptions.allowConcurrentSaves is false.
Entirely at a loss to explain it and have looked through my EF code to see whether I'm making an obvious mistake but can't see it. The bundle sent to the WebAPI SaveChanges method looks correct (correctly populated with IDs etc.) too.
https://github.com/DazWilkin/BreezeJS.ScoreIssue
UPDATE 6th February
The issue remains unresolved by Wade's helpful answer. Unfortunately, unless I can understand what it is I'm doing wrong or learn that this is a bug, I'm going to have to abandon the use of Breeze in this project and revert to crappy, plain old AJAX calls.
It would appear that the issue revolves around the server returning a zeroed GUID when saving changes. The method returns no errors. I would be thrilled to learn that this is a bug in my entity model but I'm doubtful.
Here's the failure:
breeze.debug.js: 11954
var ix = this._indexMap[tempValue];
if (ix === undefined) {
throw new Error("Internal Error in key fixup - unable to locate entity");
}
When the code reaches this point, the value of this._indexMap is correct and is:
{"bcb6e670-00fc-469d-8531-5767f40bf3c1":0}
BUT the value of tempValue (as returned from the Web API call by the server) is wrong:
00000000-0000-0000-0000-000000000000
The realValue is correct and is:
1093b975-7686-4621-8336-77c38ed36de0
Backing up the stack. Here are the results from the AJAX call, breeze.debug.js: 12574. See that the tempValue is zeroed on return from the server/WebAPI call. The realValue is correct. This is what the database contains. The row is added to the table without problem.
"KeyMappings": [
{
"$id": "4",
"$type": "Breeze.WebApi.KeyMapping, Breeze.WebApi",
"EntityTypeName": "...Score",
"TempValue": "51877f5b-811f-4260-bd5b-cf9965159597",
"RealValue": "92b73b8a-8b33-45cd-9822-ca7c0c5d5d9a"
},
{
"$id": "5",
"$type": "Breeze.WebApi.KeyMapping, Breeze.WebApi",
"EntityTypeName": "...PropertyValue",
"TempValue": "00000000-0000-0000-0000-000000000000",
"RealValue": "1093b975-7686-4621-8336-77c38ed36de0"
}
],
Verified against what's received serverside in saveBundle. NB the IDs of both entities received at the server have valid GUID IDs.
"entities": [
{
"ID": "51877f5b-811f-4260-bd5b-cf9965159597",
...
"entityAspect": {
"entityTypeName": "Score:...",
"entityState": "Added",
"originalValuesMap": {},
"autoGeneratedKey": {
"propertyName": "ID",
"autoGeneratedKeyType": "Identity"
}
}
},
{
"ID": "bcb6e670-00fc-469d-8531-5767f40bf3c1",
...
"entityAspect": {
"entityTypeName": "PropertyValue:...",
"entityState": "Added",
"originalValuesMap": {},
"autoGeneratedKey": {
"propertyName": "ID",
"autoGeneratedKeyType": "Identity"
}
}
}
],
Unsurprisingly, the values sent to the server by the AJAX call that are created in breeze.debug.js: 10494 saveBundleStringified are correct and the same as those received by the server (won't reproduce but I assure you they are).
And, from my code, when the saveChanges is called,
manager.getChanges().length == 2
manager.getChanges()[0].ID() == "51877f5b-811f-4260-bd5b-cf9965159597" (Score)
manager.getChanges()[1].ID() == "bcb6e670-00fc-469d-8531-5767f40bf3c1" (PropertyValue)
and, as expected, these match the (temp) values of the entities' IDs during saveChanges, received by the server...
What am I doing wrong?? If I had hair, I'd be tearing it out!
I've solved it.
My inconsistently applied (!) convention is to make the setters internal/private on code first types. I say inconsistently because, after feeling that I'd exhausted all possibilities, I discovered that the PropertyValue type, i.e. the one originating the error, had an internal set.
Having removed this and rebuilt the solution, the problem is solved!
So:
public Guid ID { get; internal set; }
Should be:
public Guid ID { get; set; }
Update Jan 27:
Based on your comments to Sergey's answer, you may have been trying to do something with the changed entities before the save operation completed.
Those entities remain in their changed state ... often with temporary primary and foreign keys ... until the server reports a successful save.
You probably shouldn't touch them until the save succeeds. As Sergey observes, you should locate your post-save processing in the save success callback.
return manager.saveChanges()
.then(saveSucceeded)
.fail(saveFailed);
You should not wrap the saveChanges call in a jQuery Deferred. That is a waste of time and complexity. The EntityManager.saveChanges method returns a promise that your caller can consume. The view models can add their own success and failure callbacks
dataservice.saveChanges()
.then(hooray)
.fail(sadTrombone);
Concurrent saves
I noticed in your code that you are guarding against illegal concurrent saves using the time-delay approach you found in the Todo sample.
That approach is really only suitable for the demo. It won't work at all for you if your view models need to perform some tasks when the save succeeds. It won't work because the dataservice can't return a promise to the view models with the time-delay approach.
If you need non-blocking saves, check out the "Save Queuing" plugin described in the "Concurrent Saves" topic under the "Cool Breezes" section.
Creating entities succinctly
While looking at your code, I couldn't help noticing that your entity factory methods in scoreissue.1.0.ts were a bit verbose. What you have written as:
export function Business(manager, o: IBusiness) {
var business = manager.metadataStore.getEntityType("Business").createEntity();
business.ID(breeze.core.getUuid());
business.Name(o.name);
manager.addEntity(business);
return business;
}
could be as simple as:
export function Business(manager, o: IBusiness) {
return manager.createEntity("Business", {
ID: breeze.core.getUuid(),
Name: o.name,
});
}
The EntityManager.createEntity shortcut is new since you wrote this code so don't feel bad about having missed it.
Original answer:
[Wrong direction. Preserved to make sense of DazWilkin comment that the problem is on the client.]
Where is this being generated? On the server? If so, you can subclass EFContextProvider and override SaveChangesCore. Call the base.SaveChangesCore and put a try/catch around it. Inspect the saveMap argument. The EFContextProvider is open source; I'd start digging here.
This error can occur with multiple simultaneous save requests pending at the same time both involving key generation. Does this error only occur during a save? If so try setting your SaveOptions.allowConcurrentSaves to false. If this causes a different error, (a concurrent save error) to occur then your problem definitely has to do with concurrent saves.
Hope this helps.
I have the same problem, and I've solved it with help of jQuery deferred object. My dataservice save method now looks as follows:
saveChanges = function () {
var def = $.Deferred();
if (manager.hasChanges()) {
manager.saveChanges()
.then(function () { def.resolve() })
.fail(function (error) {
handleSaveError(error);
def.reject();
});
} else {
logger.info("Nothing to save");
def.resolve();
};
return def.promise();
};
And I call it from my view models also using jQuery deferred object:
$.when(dataservice.saveChanges())
.done(function () {
...some actions
})

Resources