Skip to content

Instantly share code, notes, and snippets.

@Vergil333
Created June 14, 2023 08:34
Show Gist options
  • Save Vergil333/57c31a28171997881f3a6d28e310cfd2 to your computer and use it in GitHub Desktop.
Save Vergil333/57c31a28171997881f3a6d28e310cfd2 to your computer and use it in GitHub Desktop.
interface SObjectInterface
data class SObjectAttributes<T: SObjectType>(
val type: T,
val url: String? = null,
)
abstract class AbstractSObject<T: SObjectType>(
type: T,
) : SObjectInterface {
val attributes: SObjectAttributes<T> = SObjectAttributes(type = type)
@JsonProperty("Id") val id: String? = null
}
/**
* @property accountNumber Text(40) ,
* @property accountSource Picklist ,
* @property active__c Picklist ,
* @property annualRevenue Currency(18, 0) ,
* @property billingAddress Address ,
* @property cleanStatus Picklist ,
* @property createdById Lookup(User) ,
* @property createdDate Date/Time ,
* @property customerPriority__c Picklist ,
* @property dandbCompanyId Lookup(D&B Company) ,
* @property description Long Text Area(32000) ,
* @property dunsNumber Text(9) ,
* @property fax Fax ,
* @property id Lookup() ,
* @property industry Picklist ,
* @property isDeleted Checkbox ,
* @property jigsaw Text(20) ,
* @property jigsawCompanyId External Lookup ,
* @property lastActivityDate Date ,
* @property lastModifiedById Lookup(User) ,
* @property lastModifiedDate Date/Time ,
* @property lastReferencedDate Date/Time ,
* @property lastViewedDate Date/Time ,
* @property masterRecordId Lookup(Account) ,
* @property naicsCode Text(8) ,
* @property naicsDesc Text(120) ,
* @property name Name ,
* @property numberOfEmployees Number(8, 0) ,
* @property numberofLocations__c Number(3, 0) ,
* @property operatingHoursId Lookup(Operating Hours) ,
* @property ownerId Lookup(User) ,
* @property ownership Picklist ,
* @property parentId Hierarchy ,
* @property phone Phone ,
* @property photoUrl URL(255) ,
* @property rating Picklist ,
* @property sLAExpirationDate__c Date ,
* @property sLASerialNumber__c Text(10) ,
* @property sLA__c Picklist ,
* @property shippingAddress Address ,
* @property sic Text(20) ,
* @property sicDesc Text(80) ,
* @property site Text(80) ,
* @property systemModstamp Date/Time ,
* @property tickerSymbol Content(20) ,
* @property tradestyle Text(255) ,
* @property type Picklist ,
* @property upsellOpportunity__c Picklist ,
* @property userRecordAccessId Lookup(User Record Access) ,
* @property website URL(255) ,
* @property yearStarted Text(4) ,
*/
class Account (
@JsonProperty("AccountNumber") val accountNumber: String? = null,
@JsonProperty("AccountSource") val accountSource: String? = null,
@JsonProperty("Active__c") val active__c: String? = null,
@JsonProperty("AnnualRevenue") val annualRevenue: Double? = null,
@JsonProperty("BillingAddress") val billingAddress: Map<String, Any?>? = null,
@JsonProperty("CleanStatus") val cleanStatus: String? = null,
@JsonProperty("CreatedById") val createdById: String,
@JsonProperty("CreatedDate") val createdDate: ZonedDateTime,
@JsonProperty("CustomerPriority__c") val customerPriority__c: String? = null,
@JsonProperty("DandbCompanyId") val dandbCompanyId: String? = null,
@JsonProperty("Description") val description: String? = null,
@JsonProperty("DunsNumber") val dunsNumber: String? = null,
@JsonProperty("Fax") val fax: String? = null,
@JsonProperty("Industry") val industry: String? = null,
@JsonProperty("IsDeleted") val isDeleted: Boolean,
@JsonProperty("Jigsaw") val jigsaw: String? = null,
@JsonProperty("JigsawCompanyId") val jigsawCompanyId: String? = null,
@JsonProperty("LastActivityDate") val lastActivityDate: LocalDate? = null,
@JsonProperty("LastModifiedById") val lastModifiedById: String,
@JsonProperty("LastModifiedDate") val lastModifiedDate: ZonedDateTime,
@JsonProperty("LastReferencedDate") val lastReferencedDate: ZonedDateTime? = null,
@JsonProperty("LastViewedDate") val lastViewedDate: ZonedDateTime? = null,
@JsonProperty("MasterRecordId") val masterRecordId: String? = null,
@JsonProperty("NaicsCode") val naicsCode: String? = null,
@JsonProperty("NaicsDesc") val naicsDesc: String? = null,
@JsonProperty("Name") val name: String,
@JsonProperty("NumberOfEmployees") val numberOfEmployees: Double? = null,
@JsonProperty("NumberofLocations__c") val numberofLocations__c: Double? = null,
@JsonProperty("OperatingHoursId") val operatingHoursId: String? = null,
@JsonProperty("OwnerId") val ownerId: String,
@JsonProperty("Ownership") val ownership: String? = null,
@JsonProperty("ParentId") val parentId: Map<String, Any?>? = null,
@JsonProperty("Phone") val phone: String? = null,
@JsonProperty("PhotoUrl") val photoUrl: String? = null,
@JsonProperty("Rating") val rating: String? = null,
@JsonProperty("SLAExpirationDate__c") val sLAExpirationDate__c: LocalDate? = null,
@JsonProperty("SLASerialNumber__c") val sLASerialNumber__c: String? = null,
@JsonProperty("SLA__c") val sLA__c: String? = null,
@JsonProperty("ShippingAddress") val shippingAddress: Map<String, Any?>? = null,
@JsonProperty("Sic") val sic: String? = null,
@JsonProperty("SicDesc") val sicDesc: String? = null,
@JsonProperty("Site") val site: String? = null,
@JsonProperty("SystemModstamp") val systemModstamp: ZonedDateTime,
@JsonProperty("TickerSymbol") val tickerSymbol: String? = null,
@JsonProperty("Tradestyle") val tradestyle: String? = null,
@JsonProperty("Type") val type: String? = null,
@JsonProperty("UpsellOpportunity__c") val upsellOpportunity__c: String? = null,
@JsonProperty("UserRecordAccessId") val userRecordAccessId: String? = null,
@JsonProperty("Website") val website: String? = null,
@JsonProperty("YearStarted") val yearStarted: String? = null,
) : AbstractSObject<SObjectType>(type = SObjectType.ACCOUNT)
/**
* @property createdById Lookup(User) ,
* @property createdDate Date/Time ,
* @property id Lookup() ,
* @property isActive Checkbox ,
* @property lastModifiedById Lookup(User) ,
* @property lastModifiedDate Date/Time ,
* @property name Text(80) ,
* @property systemModstamp Date/Time ,
* @property type Picklist ,
* @property userId Lookup(User) ,
*/
class Calendar (
@JsonProperty("CreatedById") val createdById: String,
@JsonProperty("CreatedDate") val createdDate: ZonedDateTime,
@JsonProperty("IsActive") val isActive: Boolean,
@JsonProperty("LastModifiedById") val lastModifiedById: String,
@JsonProperty("LastModifiedDate") val lastModifiedDate: ZonedDateTime,
@JsonProperty("Name") val name: String,
@JsonProperty("SystemModstamp") val systemModstamp: ZonedDateTime,
@JsonProperty("Type") val type: String,
@JsonProperty("UserId") val userId: String? = null,
) : AbstractSObject<SObjectType>(type = SObjectType.CALENDAR)
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import org.springframework.web.util.UriUtils
import java.net.HttpURLConnection
import java.net.URL
import java.nio.charset.Charset
import java.time.LocalDate
import java.time.LocalTime
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import kotlin.reflect.KVisibility
import kotlin.reflect.full.findAnnotations
import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.javaField
/**
* @param accessToken has to have [web, chatter_api, api] permissions for querying
* @param instanceUrl acquired in the same response as accessToken
*
* @credits to mjg123 https://github.com/mjg123/java-http-clients/blob/master/src/main/java/com/twilio/JavaHttpURLConnectionDemo.java
* @credits to madmax1028, broot https://discuss.kotlinlang.org/t/how-to-specify-generic-output-type-to-be-subclass-of-generic-type/24637/12
*/
class SalesforceClient(
private val accessToken: String,
private val instanceUrl: String,
) {
private val apiVersion = "v53.0"
private val mapper = jacksonObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerKotlinModule()
.registerModule(
JavaTimeModule()
.addDeserializer(
ZonedDateTime::class.java,
InstantDeserializer.ZONED_DATE_TIME,
)
.addDeserializer(
LocalDate::class.java,
LocalDateDeserializer.INSTANCE,
)
.addDeserializer(
LocalTime::class.java,
LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss.SSS'Z'")),
),
)
fun <T: SObjectInterface> getAll(clazz: Class<T>): SfResponse<T> =
getResponse("SELECT ${getFields<T>(clazz).joinToString()} FROM ${clazz.simpleName}", clazz)
fun <T: SObjectInterface> count(clazz: Class<T>): Int =
getResponse("SELECT COUNT() FROM+${clazz.simpleName}", clazz).totalSize
private fun <T: SObjectInterface> getResponse(query: String, clazz: Class<T>): SfResponse<T> {
val url = constructUrl(query)
val connection: HttpURLConnection = url.openConnection() as HttpURLConnection
connection.setRequestProperty("accept", "application/json")
connection.setRequestProperty("Authorization", "Bearer $accessToken")
val wrappedResponseType = mapper.typeFactory.constructParametricType(SfResponse::class.java, clazz)
return mapper.readValue(connection.inputStream, wrappedResponseType)
}
private fun constructUrl(query: String) = URL("$instanceUrl/services/data/$apiVersion/query?q=$query")
.toUri().toURL()
@OptIn(ExperimentalStdlibApi::class)
private fun <T: SObjectInterface> getFields(clazz: Class<T>): List<String> {
val propertiesWeWant = clazz.kotlin.memberProperties
.filter { prop ->
prop.visibility == KVisibility.PUBLIC
&& listOf("java.util", "java.lang").contains(prop.javaField?.type?.packageName)
&& prop.name != "attributes"
}
val bodyProps = propertiesWeWant.mapNotNull { it.javaField?.getAnnotation(JsonProperty::class.java)?.value }
val constructorProps = clazz.kotlin.primaryConstructor?.parameters
?.filter { it.name in (propertiesWeWant.map { it.name }) }
?.map { it.findAnnotations(JsonProperty::class).first().value }
?: emptyList()
return bodyProps + constructorProps
}
/**
* URI is used for URL encoding.
*/
private fun URL.toUri() = URI(
this.protocol,
this.userInfo,
IDN.toASCII(this.host),
this.port,
this.path,
this.query, this.ref,
)
}
class SfResponse<T: SObjectInterface>(
val totalSize: Int,
val done: Boolean,
val records: List<T>,
)
enum class SObjectType(val jsonName: String) {
@JsonProperty("Account") ACCOUNT("Account"),
@JsonProperty("Calendar") CALENDAR("Calendar"),
@JsonProperty("TestObject__c") TEST_OBJECT__C("TestObject__c"),
}
/**
* @property createdById Lookup(User) ,
* @property createdDate Date/Time ,
* @property id Lookup() ,
* @property isDeleted Checkbox ,
* @property lastModifiedById Lookup(User) ,
* @property lastModifiedDate Date/Time ,
* @property name Text(80) ,
* @property ownerId Lookup(User,Group) ,
* @property systemModstamp Date/Time ,
* @property userRecordAccessId Lookup(User Record Access) ,
*/
class TestObject__c (
@JsonProperty("CreatedById") val createdById: String,
@JsonProperty("CreatedDate") val createdDate: ZonedDateTime,
@JsonProperty("IsDeleted") val isDeleted: Boolean,
@JsonProperty("LastModifiedById") val lastModifiedById: String,
@JsonProperty("LastModifiedDate") val lastModifiedDate: ZonedDateTime,
@JsonProperty("Name") val name: String? = null,
@JsonProperty("OwnerId") val ownerId: String,
@JsonProperty("SystemModstamp") val systemModstamp: ZonedDateTime,
@JsonProperty("UserRecordAccessId") val userRecordAccessId: String? = null,
) : AbstractSObject<SObjectType>(type = SObjectType.TEST_OBJECT__C)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment