Skip to content

Instantly share code, notes, and snippets.

@vkeenan
Created May 10, 2023 03:32
Show Gist options
  • Save vkeenan/2e340ca2f6f2c5e72f42c788db36f0cf to your computer and use it in GitHub Desktop.
Save vkeenan/2e340ca2f6f2c5e72f42c788db36f0cf to your computer and use it in GitHub Desktop.
How to use ChatGPT-4 as a Salesforce Developer

How to use ChatGPT-4 as a Salesforce Developer

Loading the Model

System: You are a helpful AI pair programmer who is an expert in Salesforce languages and Salesforce system integration.

I am a Salesforce developer who has an existing Apex class library that follows set patterns. I want to show this to you so you can learn from it and help me write more code like it.

To get started, here is an existing Apex class called ResearchTopic that I want to show you.

Here are some features of the class I want you to notice:

  • The class is called ResearchTopic and it is in a file called ResearchTopic.cls.
  • The class fields are public and are named the same as the database fields, except in the case of foreign keys where the field name is suffixed with Id. For example, the database field ResearchProject__c is mapped to the class field ResearchProjectId.
  • The UUID__c field is a custom field that is used to uniquely identify the record. It is used in place of the standard Id field.
public inherited sharing class ResearchTopic {
	public String ID;
	public String CreatedById;
	public DateTime CreatedDate;
	public String LastModifedById;
	public DateTime LastModifiedDate;
	public String ResearchProjectId;
	public String TopicId;

	public ResearchTopic(ResearchTopic__c theSobject) {
		unMarshalDB(theSobject);
	}

	public ResearchTopic(String recordId) {
		if (recordId != null) {
			String whereClause = (recordId.length() == 36) ? 'UUID__c = :recordId' : 'Id = :recordId';
			String queryString = 'SELECT Id, CreatedBy.UUID__c, CreatedDate, LastModifiedBy.UUID__c, LastModifiedDate, UUID__c, ResearchProject__c, Topic__c FROM ResearchTopic__c WHERE ' + whereClause + ' WITH SECURITY_ENFORCED LIMIT 1';
			List<ResearchTopic__c> sObjList = new List<ResearchTopic__c>();

			try {
				sObjList = System.Database.query(queryString);
			} catch (DmlException e) {
				System.debug('An unexpected error has occurred: ' + Utility.errormsg(e));
			}

			if (sObjList.size() == 0) {
				System.debug('ResearchTopic.constructor: unknown ResearchTopic__c ID: ' + recordId);
			} else {
				unMarshalDB(sObjList[0]);
			}
		}
	}

	private void unMarshalDB(ResearchTopic__c theSobject) {
		ID = theSobject.UUID__c;
		CreatedById = theSobject.CreatedBy.UUID__c;
		CreatedDate = theSobject.CreatedDate;
		LastModifedById = theSobject.LastModifiedBy.UUID__c;
		LastModifiedDate = theSobject.LastModifiedDate;
		ResearchProjectId = theSobject.ResearchProject__c;
		TopicId = theSobject.Topic__c;
	}

}

Let me know when you are ready to accept the parameters for a new class based on this pattern.

GPT-4 Response

I have analyzed the provided Apex class named ResearchTopic, and I understand the pattern you've described. I am ready to accept the parameters for a new class based on this pattern. Please provide the necessary details, such as the new class name, the file name, and the corresponding fields and their types.

Generate new class

The new custom object name is ResearchProduct___c, the new classname is ResearchProduct and here is the xml for the custom fields:

<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
    <fullName>CompanyProduct__c</fullName>
    <deleteConstraint>SetNull</deleteConstraint>
    <description>The Company Product for which this junction object was created</description>
    <externalId>false</externalId>
    <inlineHelpText>The Company Product for which this junction object was created</inlineHelpText>
    <label>Company Product</label>
    <referenceTo>CompanyProduct__c</referenceTo>
    <relationshipLabel>Research Products</relationshipLabel>
    <relationshipName>ResearchProducts</relationshipName>
    <required>false</required>
    <trackTrending>false</trackTrending>
    <type>Lookup</type>
</CustomField>
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
    <fullName>ProductName__c</fullName>
    <description>The Product Name from the linked CompanyProduct record</description>
    <externalId>false</externalId>
    <formula>CompanyProduct__r.Name</formula>
    <formulaTreatBlanksAs>BlankAsZero</formulaTreatBlanksAs>
    <inlineHelpText>The Product Name from the linked CompanyProduct record</inlineHelpText>
    <label>Product Name</label>
    <required>false</required>
    <trackTrending>false</trackTrending>
    <type>Text</type>
    <unique>false</unique>
</CustomField>
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
    <fullName>ResearchProject__c</fullName>
    <description>The Research Project which owns this Research Product junction record</description>
    <externalId>false</externalId>
    <inlineHelpText>The Research Project which owns this Research Product junction record</inlineHelpText>
    <label>Research Project</label>
    <referenceTo>ResearchProject__c</referenceTo>
    <relationshipLabel>Research Products</relationshipLabel>
    <relationshipName>ResearchProducts</relationshipName>
    <relationshipOrder>0</relationshipOrder>
    <reparentableMasterDetail>false</reparentableMasterDetail>
    <trackTrending>false</trackTrending>
    <type>MasterDetail</type>
    <writeRequiresMasterRead>false</writeRequiresMasterRead>
</CustomField>
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
    <fullName>UUID__c</fullName>
    <description>The unique record id for this object instance</description>
    <externalId>false</externalId>
    <inlineHelpText>The unique record id for this object instance</inlineHelpText>
    <label>UUID</label>
    <length>36</length>
    <required>false</required>
    <trackTrending>false</trackTrending>
    <type>Text</type>
    <unique>false</unique>
</CustomField>

GPT-4 Response

Based on the provided information, I have created a new Apex class named ResearchProduct following the pattern of the ResearchTopic class. Here is the code for the new ResearchProduct class:

public inherited sharing class ResearchProduct {
    public String ID;
    public String CreatedById;
    public DateTime CreatedDate;
    public String LastModifedById;
    public DateTime LastModifiedDate;
    public String CompanyProductId;
    public String ProductName;
    public String ResearchProjectId;

    public ResearchProduct(ResearchProduct__c theSobject) {
        unMarshalDB(theSobject);
    }

    public ResearchProduct(String recordId) {
        if (recordId != null) {
            String whereClause = (recordId.length() == 36) ? 'UUID__c = :recordId' : 'Id = :recordId';
            String queryString = 'SELECT Id, CreatedBy.UUID__c, CreatedDate, LastModifiedBy.UUID__c, LastModifiedDate, UUID__c, CompanyProduct__c, ProductName__c, ResearchProject__c FROM ResearchProduct__c WHERE ' + whereClause + ' WITH SECURITY_ENFORCED LIMIT 1';
            List<ResearchProduct__c> sObjList = new List<ResearchProduct__c>();

            try {
                sObjList = System.Database.query(queryString);
            } catch (DmlException e) {
                System.debug('An unexpected error has occurred: ' + Utility.errormsg(e));
            }

            if (sObjList.size() == 0) {
                System.debug('ResearchProduct.constructor: unknown ResearchProduct__c ID: ' + recordId);
            } else {
                unMarshalDB(sObjList[0]);
            }
        }
    }

    private void unMarshalDB(ResearchProduct__c theSobject) {
        ID = theSobject.UUID__c;
        CreatedById = theSobject.CreatedBy.UUID__c;
        CreatedDate = theSobject.CreatedDate;
        LastModifedById = theSobject.LastModifiedBy.UUID__c;
        LastModifiedDate = theSobject.LastModifiedDate;
        CompanyProductId = theSobject.CompanyProduct__c;
        ProductName = theSobject.ProductName__c;
        ResearchProjectId = theSobject.ResearchProject__c;
    }

}

Service Functions

Very good. you created the new service class wrapper successfully. Now I'm going to show you the service functions we need to implement next.

Please notice the features of this service class:

  • There are two static functions implemented: getById and getAllResearchProducts.
  • The getAll... functions SELECT statment must match the fields used in the unMarshalDB function.
public with sharing class ResearchProjectServices {

	public static List<ResearchProject> getById(String recordId) {
		Situs.setsize = 1;
		return new List<ResearchProject> {
			       new ResearchProject(recordId)
		};
	}

	public static List<ResearchProject> getAllResearchProjects(Integer theLimit, Integer theOffset) {
		Situs.setsize = System.Database.countQuery('SELECT count() FROM ResearchProject__c');
		List<ResearchProject__c> sObjects;
		List<ResearchProject> researchProjects = new List<ResearchProject>();
		try {
			sObjects = [
				SELECT Id, UUID__c, CreatedBy.UUID__c, CreatedDate, LastModifiedBy.UUID__c, LastModifiedDate, Account__c, Description__c, Slug__c
				FROM ResearchProject__c
				WITH SECURITY_ENFORCED
				ORDER BY Id
				LIMIT :theLimit
				OFFSET :theOffset
			];
		} catch (DmlException e) {
			System.debug(LoggingLevel.ERROR, 'An unexpected error has occurred: ' + Utility.errormsg(e));
		}

		for (ResearchProject__c theSobject : sObjects) {
			researchProjects.add(new ResearchProject(theSobject));
		}

		return researchProjects;
	}

}

Use this pattern to write a ResearchProjectServices class based on ResearchCompany class already generated.

GPT-4 Response

Based on the provided ResearchProjectServices class, I have created the ResearchProductServices class that follows the same pattern and corresponds to the ResearchProduct class. Here is the code for the new ResearchProductServices class:

public with sharing class ResearchProductServices {

    public static List<ResearchProduct> getById(String recordId) {
        Situs.setsize = 1;
        return new List<ResearchProduct> {
                   new ResearchProduct(recordId)
        };
    }

    public static List<ResearchProduct> getAllResearchProducts(Integer theLimit, Integer theOffset) {
        Situs.setsize = System.Database.countQuery('SELECT count() FROM ResearchProduct__c');
        List<ResearchProduct__c> sObjects;
        List<ResearchProduct> researchProducts = new List<ResearchProduct>();
        try {
            sObjects = [
                SELECT Id, UUID__c, CreatedBy.UUID__c, CreatedDate, LastModifiedBy.UUID__c, LastModifiedDate, CompanyProduct__c, ProductName__c, ResearchProject__c
                FROM ResearchProduct__c
                WITH SECURITY_ENFORCED
                ORDER BY Id
                LIMIT :theLimit
                OFFSET :theOffset
            ];
        } catch (DmlException e) {
            System.debug(LoggingLevel.ERROR, 'An unexpected error has occurred: ' + Utility.errormsg(e));
        }

        for (ResearchProduct__c theSobject : sObjects) {
            researchProducts.add(new ResearchProduct(theSobject));
        }

        return researchProducts;
    }

}

This class implements two static functions, getById and getAllResearchProducts, which retrieve records from the ResearchProduct__c object. The SELECT statement in the getAllResearchProducts function matches the fields used in the unMarshalDB function of the ResearchProduct class. Please review the code and let me know if you have any questions or need further assistance.

Trigger Handler

We also need a trigger handler class for the ResearchProduct__c object. Please notice the features of this trigger handler class:

  • The trigger handler class has two static functions: afterInsert and afterUpdate to ensure the UUID field is set.
public with sharing class ResearchProjectTriggerHandler {
	public static void afterInsert (Map<Id, ResearchProject__c> newMap) {
		doAfterInsert(newMap.keySet());
	}

	@future(callout = true)
	private static void doAfterInsert(Set<Id> recordIds) {
		Boolean updateFlag = false;
		List<ResearchProject__c> theSobjects = [
			SELECT
			Id,
			UUID__c
			FROM ResearchProject__c
			WHERE Id IN : recordIds
		];

		for (ResearchProject__c theSobject : theSobjects) {
			if (theSobject.UUID__c == null) {
				updateFlag = true;
				theSobject.UUID__c = Utility.newUuid();
			}
		}

		if (updateFlag) {
			Update theSobjects;
		}
	}

}

Create a trigger handler for the ResearchProduct__c object based on the ResearchProjectTriggerHandler class.

GPT-4 Response

Based on the provided ResearchProjectTriggerHandler class, I have created a trigger handler for the ResearchProduct__c object named ResearchProductTriggerHandler. Here is the code for the new ResearchProductTriggerHandler class:

public with sharing class ResearchProductTriggerHandler {
    public static void afterInsert (Map<Id, ResearchProduct__c> newMap) {
        doAfterInsert(newMap.keySet());
    }

    @future(callout = true)
    private static void doAfterInsert(Set<Id> recordIds) {
        Boolean updateFlag = false;
        List<ResearchProduct__c> theSobjects = [
            SELECT
            Id,
            UUID__c
            FROM ResearchProduct__c
            WHERE Id IN : recordIds
        ];

        for (ResearchProduct__c theSobject : theSobjects) {
            if (theSobject.UUID__c == null) {
                updateFlag = true;
                theSobject.UUID__c = Utility.newUuid();
            }
        }

        if (updateFlag) {
            Update theSobjects;
        }
    }

}

This trigger handler class has two static functions, afterInsert and doAfterInsert, to ensure the UUID field is set for the ResearchProduct__c object. Please review the code and let me know if you have any questions or need further assistance.

Trigger

We also need a trigger for the ResearchProductc object. Create a new trigger for the ResearchProductc object based on the ResearchProjectTrigger class.

trigger ResearchProjectTrigger on ResearchProject__c (after insert) {

	if (Trigger.isAfter && Trigger.isInsert) {
		ResearchProjectTriggerHandler.afterInsert(Trigger.newMap);
	}
}

GPT-4 Response

Based on the provided ResearchProjectTrigger, I have created a new trigger for the ResearchProduct__c object named ResearchProductTrigger. Here is the code for the new ResearchProductTrigger:

trigger ResearchProductTrigger on ResearchProduct__c (after insert) {

    if (Trigger.isAfter && Trigger.isInsert) {
        ResearchProductTriggerHandler.afterInsert(Trigger.newMap);
    }
}

REST Handler

The last thing we need for a new custom object in this org is a handler function for the REST API. Create a new Apex class named HttpResearchProduct based on the HttpResearchProject class.

@RestResource(urlMapping = '/researchprojects')
global with sharing class HttpResearchProject {
	@HttpGet
	global static void getResearchProjects() {
		Context.isRest = true;
		Datetime queryStart = Datetime.now();
		ResearchProjectResponse response = new ResearchProjectResponse();
		Integer theOffset = HttpHelper.getOffset();
		Integer theLimit = HttpHelper.getLimit();
		String researchProjectId = RestContext.request.params.get('researchProjectId');
		String altId = RestContext.request.params.get('id');

		if (altId != null) {
			researchProjectId = altId;
		}

		if (researchProjectId != null) {
			response.data = ResearchProjectServices.getById(researchProjectId);
		} else {
			response.data = ResearchProjectServices.getAllResearchProjects(theLimit, theOffset);
		}

		// construct response metadata
		response.meta.pagination.plimit = theLimit;
		response.meta.pagination.poffset = theOffset;
		response.meta.pagination.pagesize = response.data.size();
		response.meta.pagination.setsize = situs.setsize;
		Datetime queryEnd = Datetime.now();
		Long theTime = (queryEnd.getTime() - queryStart.getTime());
		response.meta.serverresponsetime = String.valueOf(theTime) + ' miliseconds';

		RestResponse res = RestContext.response;
		try {
			res.responseBody = Blob.valueOf(JSON.serialize(response));
			res.addHeader('Content-Type', 'application/json');
			res.statusCode = 200;
		} catch (Exception e) {
			res.responseBody = Blob.valueOf(Utility.errormsg(e));
			res.statusCode = 500;
		}
	}

}

GPT-4 Response

Based on the provided HttpResearchProject class, I have created a new Apex class named HttpResearchProduct. Here is the code for the new HttpResearchProduct class:

@RestResource(urlMapping = '/researchProducts')
global with sharing class HttpResearchProduct {
    @HttpGet
    global static void getResearchProducts() {
        Context.isRest = true;
        Datetime queryStart = Datetime.now();
        ResearchProductResponse response = new ResearchProductResponse();
        Integer theOffset = HttpHelper.getOffset();
        Integer theLimit = HttpHelper.getLimit();
        String researchProductId = RestContext.request.params.get('researchProductId');
        String altId = RestContext.request.params.get('id');

        if (altId != null) {
            researchProductId = altId;
        }

        if (researchProductId != null) {
            response.data = ResearchProductServices.getById(researchProductId);
        } else {
            response.data = ResearchProductServices.getAllResearchProducts(theLimit, theOffset);
        }

        // construct response metadata
        response.meta.pagination.plimit = theLimit;
        response.meta.pagination.poffset = theOffset;
        response.meta.pagination.pagesize = response.data.size();
        response.meta.pagination.setsize = situs.setsize;
        Datetime queryEnd = Datetime.now();
        Long theTime = (queryEnd.getTime() - queryStart.getTime());
        response.meta.serverresponsetime = String.valueOf(theTime) + ' miliseconds';

        RestResponse res = RestContext.response;
        try {
            res.responseBody = Blob.valueOf(JSON.serialize(response));
            res.addHeader('Content-Type', 'application/json');
            res.statusCode = 200;
        } catch (Exception e) {
            res.responseBody = Blob.valueOf(Utility.errormsg(e));
            res.statusCode = 500;
        }
    }

}

This new class handles the REST API requests for the ResearchProduct__c object. It has a getResearchProducts function that retrieves records based on the given parameters and returns the response in JSON format. Please review the code and let me know if you have any questions or need further assistance.

Response Class

I forgot about the Response class for the ResearchProduct__c object. Create a new Apex class named ResearchProductResponse based on the ResearchProjectResponse class.

public with sharing class ResearchProjectResponse {
	public ResponseMeta meta = new ResponseMeta();
	public List<ResearchProject> data = new List<ResearchProject>();
}

GPT-4 Response

Based on the provided ResearchProjectResponse class, I have created a new Apex class named ResearchProductResponse. Here is the code for the new ResearchProductResponse class:

public with sharing class ResearchProductResponse {
    public ResponseMeta meta = new ResponseMeta();
    public List<ResearchProduct> data = new List<ResearchProduct>();
}

This new class represents the response structure for the ResearchProduct__c object, including metadata and a list of ResearchProduct instances. Please review the code and let me know if you have any questions or need further assistance.

Analysis

Analyze available literature for patterns used by Apex developers. See if you can classify the patterns we've been using with any of those patterns. Suggest ways to use a better strategy based on your literature research.

GPT-4 Response

Based on available literature and common practices, I can classify the patterns we've been using in the code provided into the following categories:

  1. Data Access Object (DAO) pattern: The classes like ResearchProject and ResearchProduct act as a bridge between the database layer and the business logic layer. They encapsulate the logic for querying and updating the data in the database.

  2. Service layer pattern: The classes like ResearchProjectServices and ResearchProductServices provide a layer of abstraction and encapsulate the business logic for the respective objects. They implement the CRUD operations and any additional business rules specific to these objects.

  3. Trigger Handler pattern: The classes like ResearchProjectTriggerHandler and ResearchProductTriggerHandler are responsible for handling the trigger events for the respective objects. They ensure that the business logic and any required updates are performed during trigger events.

  4. REST API Handler pattern: The classes like HttpResearchProject and HttpResearchProduct handle the REST API requests for the respective objects. They provide an API interface for external systems to interact with the Salesforce data.

Although the current implementation follows several best practices, there are some suggestions for improvements:

  1. Separation of Concerns: Separate the query logic from the classes like ResearchProject and ResearchProduct into separate DAO classes. This will make the code more modular and easier to maintain.

  2. Bulkification: Ensure that all the code is optimized for handling bulk operations in triggers and services, to avoid hitting governor limits.

  3. Unit Testing: Implement comprehensive unit tests for each class, covering various scenarios and edge cases to ensure the code is reliable and maintainable.

  4. Error Handling: Improve error handling by using custom exceptions and providing more detailed error messages to the users or developers.

  5. Caching: Implement caching mechanisms to store frequently accessed data and reduce the number of queries to the database, improving performance.

  6. Use of Selector layer: Introduce a Selector layer, which is responsible for querying the data. This will help in centralizing the SOQL queries and their logic.

By incorporating these suggestions and following best practices, you can create a more robust, maintainable, and scalable Apex implementation in your Salesforce org.

Refactor

refactor available code to implement a Selector Layer for the ResearchProject object

GPT-4 Response

To implement a Selector layer for the ResearchProject object, we need to create a new class, named ResearchProjectSelector, that will handle all the queries related to the ResearchProject__c object. This class will centralize the SOQL queries and their logic, improving maintainability.

First, let's create the ResearchProjectSelector class:

public with sharing class ResearchProjectSelector {
    public static List<ResearchProject__c> getById(String recordId) {
        String whereClause = (recordId.length() == 36) ? 'UUID__c = :recordId' : 'Id = :recordId';
        String queryString = 'SELECT Id, UUID__c, CreatedBy.UUID__c, CreatedDate, LastModifiedBy.UUID__c, LastModifiedDate, Account__c, Description__c, Slug__c FROM ResearchProject__c WHERE ' + whereClause + ' WITH SECURITY_ENFORCED LIMIT 1';
        return Database.query(queryString);
    }

    public static List<ResearchProject__c> getAll(Integer theLimit, Integer theOffset) {
        return [
            SELECT Id, UUID__c, CreatedBy.UUID__c, CreatedDate, LastModifiedBy.UUID__c, LastModifiedDate, Account__c, Description__c, Slug__c
            FROM ResearchProject__c
            WITH SECURITY_ENFORCED
            ORDER BY Id
            LIMIT :theLimit
            OFFSET :theOffset
        ];
    }

    public static Integer countAll() {
        return Database.countQuery('SELECT count() FROM ResearchProject__c');
    }
}

Now, let's refactor the ResearchProject class to use the Selector layer:

public inherited sharing class ResearchProject {
    // ...

    public ResearchProject(String recordId) {
        if (recordId != null) {
            List<ResearchProject__c> sObjList = ResearchProjectSelector.getById(recordId);

            if (sObjList.size() == 0) {
                System.debug('ResearchProject.constructor: unknown ResearchProject__c ID: ' + recordId);
            } else {
                unMarshalDB(sObjList[0]);
            }
        }
    }

    // ...
}

Finally, let's refactor the ResearchProjectServices class to use the Selector layer:

public with sharing class ResearchProjectServices {

    public static List<ResearchProject> getById(String recordId) {
        Situs.setsize = 1;
        return new List<ResearchProject> {
                   new ResearchProject(recordId)
        };
    }

    public static List<ResearchProject> getAllResearchProjects(Integer theLimit, Integer theOffset) {
        Situs.setsize = ResearchProjectSelector.countAll();
        List<ResearchProject__c> sObjects = ResearchProjectSelector.getAll(theLimit, theOffset);
        List<ResearchProject> researchProjects = new List<ResearchProject>();

        for (ResearchProject__c theSobject : sObjects) {
            researchProjects.add(new ResearchProject(theSobject));
        }

        return researchProjects;
    }
}
@delolmoa
Copy link

Hi Vernon,

Congratulations on your article.
Have you tried running similar tests with Google Gemini?

Thanks,
Ángel

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment