13

I am using the LWC Data service to get query an Account already in the component via

@track record;

@wire(getRecord, { recordId: "$recordId", fields: ...})
setRecord({error, data}) {
    // Need to do it with a method as sometimes recordId is empty
    if(!error && data) {
        this.record = data;
    }
}

and later pass it to an Apex method using:

 callApexMethod({account : record}) {
    ... 
 }

which looks like

@AuraEnabled(cacheable=true)
public static String myApexMethod(SObject account) {
   ...
}

This looks similar to this Known Issue but this should be fixed for a year.

2
  • Have you considered using Account instead of SObject? Commented Aug 7, 2019 at 12:09
  • it's baffling that the team that implement this would fail to consider this. Just shows how out of touch their teams can be with actual salesforce development.
    – NSjonas
    Commented Aug 7, 2019 at 16:34

5 Answers 5

12

The data you get from wire service is in the form as below:

{
  "apiName": "Account",
  "childRelationships": {
    
  },
  "fields": {
    "Name": {
      "displayValue": null,
      "value": "University of Boston"
    },
    "POC_Datetime__c": {
      "displayValue": "8/7/2019 8:02 PM",
      "value": "2019-08-08T03:02:37.000Z"
    },
    "Phone": {
      "displayValue": null,
      "value": "1234567890"
    },
    "Type": {
      "displayValue": "Prospect Translated",
      "value": "Prospect"
    }
  },
  "id": "00128000009j45sAAA",
  "lastModifiedById": "00528000001IIBvAAO",
  "lastModifiedDate": "2019-08-08T07:44:29.000Z",
  "recordTypeInfo": null,
  "systemModstamp": "2019-08-08T07:44:29.000Z"
}

However, when we want to update an sObject in Apex by below method:

public static Account updateAccount(Account acc){
    System.debug('acc => '+JSON.serialize(acc));
    update acc;
    return acc;
}

The JSON we get is as below:

{
  "attributes": {
    "type": "Account",
    "url": "/services/data/v46.0/sobjects/Account/00128000009j45sAAA"
  },
  "Id": "00128000009j45sAAA",
  "Name": "University of Arizona",
  "Type": "Prospect",
  "Phone": "1234567890"
}

As we can clearly see the difference, the format returned by data in wired service is totally different compared to the format needed by update DML statement in Apex.

----- added based on comments------

Taking the cues from Aura attributes we can send the record data as below:

let acc = {
    sobjectType: 'Account',
    Id: '00128000009j45sAAA',
    Name: 'University of Boston',
    Phone: '1231231232'
};

To get this format we can use below function (you can have it in utils module):

function getSObject(wiredData) {
    let sObject = {
        sobjectType: wiredData.apiName,
        Id: wiredData.id
    };
    Object.keys(wiredData.fields).map(fieldPath => {
        sObject[fieldPath] = wiredData.fields[fieldPath].value;
    });
    return sObject;
}

Now getting the record data is simply let sObject = getSObject(wiredData);

--- added based on @sfdcfox answer (with correction) -----

we can also use function as below:

function getSObject(wiredData) {
    return {
        sobjectType: wiredData.apiName,
        Id: wiredData.id,
        ...Object.keys(wiredData.fields).reduce((a, f) => {
            a[f] = wiredData.fields[f].value;
            return a;
        }, {})
    };
}
5
  • Ok, what do we do then to get it into the right format? Does Salesforce really create LWC Data Services to reduce boilerplate code but then forget to convert the data so we have to write our own cumbersome JS code to convert structures? Commented Aug 7, 2019 at 14:46
  • As of now, I am not sure whether there is better approach than forming the required object in javascript with fieldname-value pairs using the data. Commented Aug 7, 2019 at 14:52
  • @RobertSösemann Array.prototype.map is what you would use to convert the days. It's really just a one-liner, although still annoying.
    – sfdcfox
    Commented Aug 7, 2019 at 18:28
  • @sfdcfox would you mind adding an answer with a code snippet on how to convert the wire data to something that can be passed to Apex which expects an SObject? Commented Aug 7, 2019 at 18:49
  • @sfdcfox; @Robert Sosemann... :-) D, I added the answer yesterday night after my comment, but forgot to click Save Edits. (tiring day :-/) Commented Aug 8, 2019 at 3:57
6

Converting from LWC wire format to AuraEnabled format would probably look something like the following:

let record = {
  ...Object.keys(data.fields).reduce((a,f)=>a[f]=data.fields[f].value,{}),
  sobjectType: data.apiName
};

To fully understand this, you'll want to read about ... (the spread operator), Object.keys, and arrow functions.

We need to do this transformation because the objects are different "shapes". Why Salesforce decided to do this is anyone's guess, but at least it is fairly trivial to convert the data appropriately.

4
  • 2
    I think (I say think to be on safer side while talking to experts like you :-/) this will not work unless you have Id in the record. I agree that this should give desired object structure but somehow its giving me the structure like {"0":"C","1":"u","2":"s","3":"t","4":"o","5":"m","6":"e","7":"r","8":" ","9":"-","10":" ","11":"D","12":"i","13":"r","14":"e","15":"c","16":"t","sobjectType":"Account"}. I am trying to check what is the problem Commented Aug 8, 2019 at 4:00
  • understood the problem, the assignment cannot be returned directly.... added in the answer Commented Aug 8, 2019 at 4:40
  • 1
    @sfdcfox I do not think this approach is correct for all situations/fieldtypes . Please correct me but since the data is received via User Interface API, Datetimes would need to be corrected for TimeZone and Picklist Values would be received translated. Commented Aug 8, 2019 at 5:26
  • 1
    @ChristianSzandorKnapp, I have modified in my answer the structure of wired data. It does not seem to be a problem for picklist as we can map value<=>label in select options. Also for datetime, lightning-input will take care of localisation and correct value will be saved in database when changed Commented Aug 8, 2019 at 7:57
3

There is a small limitation with the code provided by @sfdcfox and @salesforce-sas. It works fine for non-relationship fields, however, once you start accessing a related record's values from the current object; it stops working.

const data = {
   "apiName":"Case",
   "childRelationships":{},
   "id":"500R000000AbnzLIAR",
   "lastModifiedById":"00541000008D4ZxAAK",
   "lastModifiedDate":"2020-03-07T05:53:12.000Z",
   "recordTypeId":"01241000001AT6fAAG",
   "recordTypeInfo":{
      "available":true,
      "defaultRecordTypeMapping":true,
      "master":false,
      "name":"CAG Cases",
      "recordTypeId":"01241000001AT6fAAG"
   },
   "systemModstamp":"2020-03-07T05:53:12.000Z",
   "fields":{
      "Account":{
         "displayValue":"Bob the Builder",
         "value":{
            "apiName":"Account",
            "childRelationships":{},
            "id":"001R000001QN4GRIA1",
            "lastModifiedById":"00541000008D4ZxAAK",
            "lastModifiedDate":"2020-03-07T04:25:36.000Z",
            "recordTypeId":"012410000012ujhAAA",
            "recordTypeInfo":{
               "available":true,
               "defaultRecordTypeMapping":false,
               "master":false,
               "name":"Billing",
               "recordTypeId":"012410000012ujhAAA"
            },
            "systemModstamp":"2020-03-07T04:25:36.000Z",
            "fields":{
               "Name":{
                  "displayValue":null,
                  "value":"Bob the Builder"
               }
            }
          }
      },
      "AccountId":{
         "displayValue":null,
         "value":"001R000001QN4GRIA1"
      },
      "Handle__c":{
         "displayValue":null,
         "value":null
      },
      "Origin":{
         "displayValue":"Manual",
         "value":"Manual"
      },
      "Person_Billing_Account__c":{
         "displayValue":null,
         "value":null
      },
      "Person_Billing_Account__r":{
         "displayValue":null,
         "value":null
      },
      "RecordTypeId":{
         "displayValue":null,
         "value":"01241000001AT6fAAG"
      }
   }
}

Above is some JSON.stringified real data returned to me by LWC's getRecord method. As you can see within the fields object, the assumption that every key's value is what we need to send to the back end (which is baked into the other code) is accurate- with the exception of Account. Its value is another object which contains its own fields, id, etc. I leveraged @sfdcfox's code and made an adjustment so we can handle values one relationship away. I'll leave it to whomever comes after me to support walking through > 1 relationship away.

One thing to note is I requested Person_Billing_Account__r.Name from getRecord, but since Person_Billing_Account__c was null, Person_Billing_Acccount__r's value is just null- instead of an object leading to null. To account for this, I just suppress all null values in my code since passing them to the controller doesn't help anything anyway.

Without further ado, here is my adjusted code:

const convertWiredDataToApexSobject = wiredData => {
  return {
    sobjectType: wiredData.apiName,
    Id: wiredData.id,
    ...extractValues(wiredData.fields)
  };
};

const extractValues = fields => {
  return Object.keys(fields).reduce((a, f) => {
    let { value } = fields[f];
    //check if value is object
    if (typeof value === "object" && value !== null)
      value = {
        Id: value.id,
        ...extractValues(value.fields)
      };
    //suppress nulls
    if (value === null) return a;
    a[f] = value;
    return a;
  }, {});
};

Usage:

//const data = <data from getRecord>
const ApexSobject = convertWiredDataToApexSobject(data);
console.log(JSON.stringify(ApexSobject))

Result:

{
   "sobjectType":"Case",
   "Id":"500R000000AbnzLIAR",
   "Account":{
      "Id":"001R000001QN4GRIA1",
      "Name":"Bob the Builder"
   },
   "AccountId":"001R000001QN4GRIA1",
   "Origin":"Manual",
   "RecordTypeId":"01241000001AT6fAAG"
}
1
  • liked your approach to avoid null value properties, how about take out system fields like(createdby,lastmodifiedby,ownerId,owner), i knew some times these fields values are needed for UI side control but while update same object it's breaking
    – sdandamud1
    Commented Oct 15, 2021 at 16:31
1

If someone is looking for a solution to make a sObject type data with Custom related object record here is how I did it:

getSobjectFromWireData (wiredData){

    let sObject = {
        sobjectType: wiredData.apiName,
        Id: wiredData.id
    };
    Object.keys(wiredData.fields).map(fieldPath => {
        if (fieldPath.includes("__r")) {
            console.log(fieldPath);
            let relatedRecord = wiredData.fields[fieldPath].value;

            console.log(relatedRecord.id);
            let tempObject={} ;
            Object.keys(relatedRecord.fields).map(relationPath => {
                tempObject[relationPath] =relatedRecord.fields[relationPath].value;

            });
            tempObject.Id =relatedRecord.id;
            console.log(tempObject);

            sObject[fieldPath] =tempObject;
        }
        else {
            sObject[fieldPath] = wiredData.fields[fieldPath].value;
        }

    });
    console.log('SObject in fucntion ' +sObject);
    return sObject;

}
1

There seems to be new support from salesforce for this problem: generateRecordInputForUpdate

import { generateRecordInputForUpdate } from 'lightning/uiRecordApi';

...

const record = generateRecordInputForUpdate(objectWithFieldsProperty).fields;

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .