├── .gitignore
├── src
├── classes
│ ├── Condition.cls
│ ├── Operand.cls
│ ├── InvalidQueryException.cls
│ ├── QueryElement.cls
│ ├── Condition.cls-meta.xml
│ ├── Field.cls-meta.xml
│ ├── Metadata.cls-meta.xml
│ ├── Operand.cls-meta.xml
│ ├── Query.cls-meta.xml
│ ├── QueryTest.cls-meta.xml
│ ├── SortInfo.cls-meta.xml
│ ├── OperandTypes.cls-meta.xml
│ ├── QueryElement.cls-meta.xml
│ ├── CompositeCondition.cls-meta.xml
│ ├── ConditionBuilder.cls-meta.xml
│ ├── MetadataValidator.cls-meta.xml
│ ├── NegateCondition.cls-meta.xml
│ ├── SObjectDescription.cls-meta.xml
│ ├── SingleCondition.cls-meta.xml
│ ├── InvalidQueryException.cls-meta.xml
│ ├── SObjectFieldDescription.cls-meta.xml
│ ├── SObjectDescription.cls
│ ├── ConditionBuilder.cls
│ ├── NegateCondition.cls
│ ├── SingleCondition.cls
│ ├── SortInfo.cls
│ ├── Field.cls
│ ├── CompositeCondition.cls
│ ├── MetadataValidator.cls
│ ├── Query.cls
│ ├── SObjectFieldDescription.cls
│ ├── QueryTest.cls
│ ├── OperandTypes.cls
│ └── Metadata.cls
└── package.xml
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | config/*
2 | debug/*
3 |
4 |
--------------------------------------------------------------------------------
/src/classes/Condition.cls:
--------------------------------------------------------------------------------
1 | public interface Condition extends QueryElement {
2 | }
--------------------------------------------------------------------------------
/src/classes/Operand.cls:
--------------------------------------------------------------------------------
1 | public interface Operand extends QueryElement {
2 | void validate();
3 | }
--------------------------------------------------------------------------------
/src/classes/InvalidQueryException.cls:
--------------------------------------------------------------------------------
1 | public with sharing class InvalidQueryException extends Exception {}
2 |
--------------------------------------------------------------------------------
/src/classes/QueryElement.cls:
--------------------------------------------------------------------------------
1 | public interface QueryElement {
2 | String toSOQL();
3 | void validate();
4 | }
--------------------------------------------------------------------------------
/src/classes/Condition.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 33.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/src/classes/Field.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 33.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/src/classes/Metadata.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 32.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/src/classes/Operand.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 33.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/src/classes/Query.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 33.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/src/classes/QueryTest.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 33.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/src/classes/SortInfo.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 33.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/src/classes/OperandTypes.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 33.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/src/classes/QueryElement.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 33.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/src/classes/CompositeCondition.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 33.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/src/classes/ConditionBuilder.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 33.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/src/classes/MetadataValidator.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 33.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/src/classes/NegateCondition.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 33.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/src/classes/SObjectDescription.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 32.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/src/classes/SingleCondition.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 33.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/src/classes/InvalidQueryException.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 33.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/src/classes/SObjectFieldDescription.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 32.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/src/classes/SObjectDescription.cls:
--------------------------------------------------------------------------------
1 | public with sharing class SObjectDescription {
2 |
3 | @AuraEnabled
4 | public String name { get; set; }
5 | @AuraEnabled
6 | public String label { get; set; }
7 | @AuraEnabled
8 | public SObjectFieldDescription[] fields { get; set; }
9 |
10 | public SObjectDescription(String name, String label) {
11 | this.name = name;
12 | this.label = label;
13 | }
14 |
15 | public SObjectDescription(String name, String label, SObjectFieldDescription[] fields) {
16 | this.name = name;
17 | this.label = label;
18 | this.fields = fields;
19 | }
20 |
21 | }
--------------------------------------------------------------------------------
/src/classes/ConditionBuilder.cls:
--------------------------------------------------------------------------------
1 | public with sharing class ConditionBuilder {
2 | public static Condition buildCondition(Map config, String baseObjectName, Boolean stripParens) {
3 | if (config.get('field') != null) {
4 | return new SingleCondition(config, baseObjectName);
5 | } else {
6 | String operator = (String) config.get('operator');
7 | if (operator != null && operator.toUpperCase() == 'NOT') {
8 | return new NegateCondition(config, baseObjectName, stripParens);
9 | } else {
10 | return new CompositeCondition(config, baseObjectName, stripParens);
11 | }
12 | }
13 | return null;
14 | }
15 | }
--------------------------------------------------------------------------------
/src/package.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | CompositeCondition
5 | Condition
6 | ConditionBuilder
7 | Field
8 | InvalidQueryException
9 | Metadata
10 | MetadataValidator
11 | NegateCondition
12 | Operand
13 | OperandTypes
14 | Query
15 | QueryElement
16 | QueryTest
17 | SObjectDescription
18 | SObjectFieldDescription
19 | SingleCondition
20 | SortInfo
21 | ApexClass
22 |
23 | 34.0
24 |
25 |
--------------------------------------------------------------------------------
/src/classes/NegateCondition.cls:
--------------------------------------------------------------------------------
1 | public with sharing class NegateCondition implements Condition {
2 | public String operator { get; set; }
3 | public Condition condition { get; set; }
4 | public Boolean stripParens { get; set; }
5 |
6 | public NegateCondition(Map config, String baseObjectName, Boolean stripParens) {
7 | this.operator = (String) config.get('operator');
8 | Map condition = (Map) config.get('condition');
9 | if (condition != null) {
10 | this.condition = ConditionBuilder.buildCondition(condition, baseObjectName, true);
11 | }
12 | this.stripParens = stripParens;
13 | }
14 |
15 | public void validate() {
16 | if (this.condition == null) {
17 | throw new InvalidQueryException('Negating condition is not defined');
18 | }
19 | this.condition.validate();
20 | }
21 |
22 | public String toSOQL() {
23 | String condition = 'NOT ' + this.condition.toSOQL();
24 | return !this.stripParens ? '(' + condition + ')' : condition;
25 | }
26 | }
--------------------------------------------------------------------------------
/src/classes/SingleCondition.cls:
--------------------------------------------------------------------------------
1 | public with sharing class SingleCondition implements Condition {
2 | public static Pattern OPERATOR_PATTERN = Pattern.compile('(?i)^(=|!=|>|>=|<|<=|LIKE|IN|NOT IN)$');
3 | public String operator { get; set; }
4 | public Field field { get; set; }
5 | public Operand value { get; set; }
6 |
7 | public SingleCondition(Map config, String baseObjectName) {
8 | this.operator = (String) config.get('operator');
9 | this.field = new Field((String) config.get('field'), baseObjectName);
10 | this.value = OperandTypes.createOperand(config.get('value'));
11 | }
12 |
13 | public void validate() {
14 | this.field.validate();
15 | if (this.operator == null || !OPERATOR_PATTERN.matcher(this.operator).matches()) {
16 | throw new InvalidQueryException('Invalid condition operator : ' + this.operator);
17 | }
18 | if (this.value == null) {
19 | throw new InvalidQueryException('Condition operand is not available');
20 | } else {
21 | this.value.validate();
22 | }
23 | }
24 |
25 | public String toSOQL() {
26 | return field.toSOQL() + ' ' + operator + ' ' + value.toSOQL();
27 | }
28 |
29 | }
--------------------------------------------------------------------------------
/src/classes/SortInfo.cls:
--------------------------------------------------------------------------------
1 | public with sharing class SortInfo implements QueryElement {
2 | public Field field { get; set; }
3 | public String direction { get; set; }
4 | public String nullOrder { get; set; }
5 |
6 | public SortInfo(Map config, String baseObjectName) {
7 | this.field = new Field((String) config.get('field'), baseObjectName);
8 | this.direction = (String) config.get('direction');
9 | this.nullOrder = (String) config.get('nullOrder');
10 | }
11 |
12 | public void validate() {
13 | this.field.validate();
14 | if (this.direction == null ||
15 | (this.direction.toUpperCase() != 'ASC' && this.direction.toUpperCase() != 'DESC') ) {
16 | throw new InvalidQueryException('Sort direction is not valid : ' + this.direction);
17 | }
18 | if (this.nullOrder != null &&
19 | this.nullOrder.toUpperCase() != 'LAST' && this.nullOrder.toUpperCase() != 'FIRST') {
20 | throw new InvalidQueryException('Sort null order is invalid : ' + this.nullOrder);
21 | }
22 | }
23 |
24 | public String toSOQL() {
25 | String str = this.field.toSOQL() + ' ' + this.direction.toUpperCase();
26 | if (this.nullOrder != null) {
27 | str += ' NULLS ' + this.nullOrder.toUpperCase();
28 | }
29 | return str;
30 | }
31 | }
--------------------------------------------------------------------------------
/src/classes/Field.cls:
--------------------------------------------------------------------------------
1 | public with sharing class Field implements QueryElement {
2 | public String[] path { get; set; }
3 | public String baseObjectName { get; set; }
4 |
5 | public Field(String path, String baseObjectName) {
6 | this.path = path.split('\\.');
7 | this.baseObjectName = baseObjectName;
8 | }
9 |
10 | public void validate() {
11 | System.debug('Validating field: ' + this.path);
12 | String sobjName = this.baseObjectName;
13 | Integer len = this.path.size();
14 | for (Integer i = 0; i < len; i++) {
15 | String refName = this.path[i];
16 | System.debug('Check if sobject ' + sobjName + '.' + refName + ' is accessible field');
17 | if (i == len - 1) {
18 | if (!MetadataValidator.isFieldAccessible(refName, sobjName)) {
19 | throw new InvalidQueryException('Field is not available or not accessible: ' + sobjName + '.' + refName);
20 | }
21 | } else {
22 | String[] parents = MetadataValidator.getParentRelationshipObjects(refName, sobjName);
23 | for (String parent: parents) {
24 | System.debug('parent = ' + parent);
25 | }
26 | if (parents == null) {
27 | throw new InvalidQueryException('Parent relationship is not available or not accessible: ' + sobjName + '.' + refName);
28 | } else {
29 | sobjName = parents.size() == 1 ? parents[0] : 'Name';
30 | }
31 | }
32 | }
33 | }
34 |
35 | public String toSOQL() {
36 | return String.join(this.path, '.');
37 | }
38 | }
--------------------------------------------------------------------------------
/src/classes/CompositeCondition.cls:
--------------------------------------------------------------------------------
1 | public with sharing class CompositeCondition implements Condition {
2 | public static Pattern OPERATOR_PATTERN = Pattern.compile('(?i)^(AND|OR)$');
3 | public String operator { get; set; }
4 | public Condition[] conditions { get; set; }
5 | public Boolean stripParens { get; set; }
6 |
7 | public CompositeCondition(Map config, String baseObjectName, Boolean stripParens) {
8 | this.operator = (String) config.get('operator');
9 | Object[] conditions = (Object[]) config.get('conditions');
10 | if (conditions != null) {
11 | this.conditions = new Condition[] {};
12 | for (Object c : conditions) {
13 | Map cond = (Map) c;
14 | this.conditions.add(ConditionBuilder.buildCondition(cond, baseObjectName, false));
15 | }
16 | }
17 | this.stripParens = stripParens;
18 | }
19 |
20 | public void validate() {
21 | System.debug('Validating Composite Condition: ' + this.operator);
22 | if (this.operator == null || !OPERATOR_PATTERN.matcher(this.operator).matches()) {
23 | throw new InvalidQueryException('Invalid logical operator : ' + this.operator);
24 | }
25 | if (this.conditions == null || this.conditions.size() == 0) {
26 | throw new InvalidQueryException('Composite conditions definitions are not found');
27 | }
28 | for (Condition condition : this.conditions) {
29 | condition.validate();
30 | }
31 | }
32 |
33 | public String toSOQL() {
34 | String[] conditionStrings = new String[] {};
35 | for (Condition cond : this.conditions) {
36 | conditionStrings.add(cond.toSOQL());
37 | }
38 | String condition = String.join(conditionStrings, ' ' + this.operator.toUpperCase() + ' ');
39 | return conditionStrings.size() > 0 && !this.stripParens ? '(' + condition + ')' : condition;
40 | }
41 |
42 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # soql-secure
2 | A library to build/execute SOQL from JSON definition in Apex with secure FLS check
3 |
4 | ## Usage
5 |
6 | #### Server(Apex)
7 |
8 | ```java
9 | public with sharing class MyRemoteController {
10 | @RemoteAction
11 | public static List query(String queryJSON) {
12 | Map qconfig = (Map) JSON.deserializeUntyped(queryJSON);
13 | Query query = new Query(qconfig);
14 | query.validate(); // HERE we check FLS and other access control
15 | return Database.query(query.toSOQL());
16 | }
17 | }
18 | ```
19 |
20 | ### Client (JavaScript in Visualforce)
21 |
22 | ```javascript
23 | // Define query in JSON
24 | var queryConfig = {
25 | "fields": [ "Id", "Name", "Account.Name" ],
26 | "table": "Contact",
27 | "condition": {
28 | "operator": "AND",
29 | "conditions": [{
30 | "field": "CloseDate",
31 | "operator": "=",
32 | "value": { "type": "date", "value": "THIS_MONTH" }
33 | }, {
34 | "field": "Amount",
35 | "operator": ">",
36 | "value": 20000
37 | }]
38 | },
39 | "sortInfo": [{
40 | "field": "CloseDate",
41 | "direction": "ASC"
42 | }],
43 | "limit": 10000
44 | };
45 | // Pass the query config to Apex through JavaScript Remoting. JSON should be serialized in advance.
46 | MyRemoteController.query(JSON.stringify(queryConfig), function(records, event) {
47 | if (event.status) {
48 | console.log(records);
49 | } else {
50 | console.error(event.message + ': ' + event.where);
51 | }
52 | });
53 | ```
54 |
55 | ## Comparison between other solutions
56 |
57 | 1. REST/SOAP API
58 | - Consumes API request quota
59 |
60 | 2. JavaScript Remoting
61 | - Doesn't have enough flexibility to build query in client side (JavaScript)
62 |
63 | 3. RemoteTK
64 | - Not secure because it doesn't check FLS, cannot be used in production
65 | - Recently removed from official Toolkit
66 |
67 | 4. Visualforce Remote Objects
68 | - Target Object/Field must be pre-defined in Page definition
69 | - Not supporting relationship query
70 |
71 | ## License
72 |
73 | MIT
74 |
75 |
--------------------------------------------------------------------------------
/src/classes/MetadataValidator.cls:
--------------------------------------------------------------------------------
1 | public with sharing class MetadataValidator {
2 |
3 | private static Map cache = new Map {};
4 |
5 | private static DescribeSObjectResult describeSObject(String sobjName) {
6 | sobjName = sobjName.toLowerCase();
7 | DescribeSObjectResult res = MetadataValidator.cache.get(sobjName);
8 | if (res == null) {
9 | SObject so = (SObject) Type.forName(sobjName).newInstance();
10 | res = so.getSObjectType().getDescribe();
11 | MetadataValidator.cache.put(sobjName, res);
12 | }
13 | return res;
14 | }
15 |
16 | public static Boolean isObjectAccessible(String sobjName) {
17 | try {
18 | SObjectType soType = ((SObject) Type.forName(sobjName).newInstance()).getSObjectType();
19 | return soType.getDescribe().isAccessible();
20 | } catch(Exception e) {
21 | return false;
22 | }
23 | return true;
24 | }
25 |
26 | public static Boolean isFieldAccessible(String fieldName, String sobjName) {
27 | if (isObjectAccessible(sobjName)) {
28 | DescribeSObjectResult sobj = describeSObject(sobjName);
29 | Map fields = sobj.fields.getMap();
30 | for (SObjectField f : fields.values()) {
31 | DescribeFieldResult field = f.getDescribe();
32 | if (field.getName().toLowerCase() == fieldName.toLowerCase()) {
33 | return field.isAccessible();
34 | }
35 | }
36 | }
37 | return false;
38 | }
39 |
40 | /*
41 | public static String getChildRelationshipObject(String childRelName, String sobjName) {
42 | if (isObjectAccessible(sobjName)) {
43 | DescribeSObjectResult sobj = describeSObject(sobjName);
44 | ChildRelationship[] childRels = sobj.getChildRelationships();
45 | for (ChildRelationship childRel : childRels) {
46 | if (childRel.getRelationshipName().toLowerCase() == childRelName.toLowerCase()) {
47 | return childRel.getChildSObject().getDescribe().getName();
48 | }
49 | }
50 | }
51 | return null;
52 | }
53 | */
54 |
55 | public static String[] getParentRelationshipObjects(String parentRelName, String sobjName) {
56 | if (isObjectAccessible(sobjName)) {
57 | DescribeSObjectResult sobj = describeSObject(sobjName);
58 | Map fields = sobj.fields.getMap();
59 | for (SObjectField f : fields.values()) {
60 | DescribeFieldResult field = f.getDescribe();
61 | String relationshipName = field.getRelationshipName();
62 | if (relationshipName != null &&
63 | relationshipName.toLowerCase() == parentRelName.toLowerCase() &&
64 | isFieldAccessible(field.getName(), sobjName)) {
65 | String[] parents = new String[] {};
66 | for (SObjectType refType : field.getReferenceTo()) {
67 | parents.add(refType.getDescribe().getName());
68 | }
69 | return parents;
70 | }
71 | }
72 | }
73 | return null;
74 | }
75 |
76 | }
--------------------------------------------------------------------------------
/src/classes/Query.cls:
--------------------------------------------------------------------------------
1 | public with sharing class Query implements QueryElement {
2 | public Field[] fields { get; set; }
3 | public String table { get; set; }
4 | public Condition condition { get; set; }
5 | public SortInfo[] sortInfo { get; set; }
6 | public Integer queryLimit { get; set; }
7 | public Integer offset { get; set; }
8 |
9 | public Query(Map config) {
10 | this.table = (String) config.get('table');
11 | System.debug('table = ' + this.table);
12 | this.fields = new Field[] {};
13 | Object[] fields = (Object[]) config.get('fields');
14 | if (fields != null) {
15 | for (Object field : fields) {
16 | System.debug('field = ' + field);
17 | this.fields.add(new Field((String) field, this.table));
18 | }
19 | }
20 | Map cond = (Map) config.get('condition');
21 | if (cond != null) {
22 | this.condition = ConditionBuilder.buildCondition(cond, this.table, true);
23 | }
24 | Object[] sortInfo = (Object[]) config.get('sortInfo');
25 | if (sortInfo != null) {
26 | this.sortInfo = new SortInfo[] {};
27 | for (Object s: sortInfo) {
28 | this.sortInfo.add(new SortInfo((Map) s, this.table));
29 | }
30 | }
31 | this.queryLimit = (Integer) config.get('limit');
32 | this.offset = (Integer) config.get('offset');
33 | }
34 |
35 | public void validate() {
36 | System.debug('Validating Table: ' + this.table);
37 | if (!MetadataValidator.isObjectAccessible(this.table)) {
38 | throw new InvalidQueryException('Querying object is not available or not accessible : ' + this.table);
39 | }
40 | if (fields == null) { throw new InvalidQueryException('fields are not defined in query config'); }
41 | System.debug('Validating Fields: ');
42 | for (Field field : this.fields) {
43 | field.validate();
44 | }
45 | System.debug('Validating Conditions: ');
46 | if (this.condition != null) {
47 | this.condition.validate();
48 | }
49 | System.debug('Validating Sort Info: ');
50 | if (this.sortInfo != null) {
51 | for (SortInfo sortInfo : this.sortInfo) {
52 | sortInfo.validate();
53 | }
54 | }
55 | System.debug('Validating Query Limit and Offset: ');
56 | if (this.queryLimit != null && this.queryLimit <= 0) {
57 | throw new InvalidQueryException('Query limit should be plus integer: ' + this.queryLimit);
58 | }
59 | if (this.offset != null && this.offset <= 0) {
60 | throw new InvalidQueryException('Query offset should be plus integer: ' + this.queryLimit);
61 | }
62 | }
63 |
64 | public String toSOQL() {
65 | String[] fieldStrings = new String[] {};
66 | for (Field field : this.fields) {
67 | fieldStrings.add(field.toSOQL());
68 | }
69 | String soql = 'SELECT ' + String.join(fieldStrings, ', ') + ' FROM ' + this.table;
70 | if (this.condition != null) {
71 | soql += ' WHERE ' + this.condition.toSOQL();
72 | }
73 | if (this.sortInfo != null && this.sortInfo.size() > 0) {
74 | String[] sortInfoStrings = new String[] {};
75 | for (SortInfo si: this.sortInfo) {
76 | sortInfoStrings.add(si.toSOQL());
77 | }
78 | soql += ' ORDER BY ' + String.join(sortInfoStrings, ', ');
79 | }
80 | if (this.queryLimit != null) {
81 | soql += ' LIMIT ' + this.queryLimit;
82 | }
83 | return soql;
84 | }
85 |
86 | public List execute() {
87 | this.validate();
88 | String soql = this.toSOQL();
89 | return Database.query(soql);
90 | }
91 |
92 | }
--------------------------------------------------------------------------------
/src/classes/SObjectFieldDescription.cls:
--------------------------------------------------------------------------------
1 | public with sharing class SObjectFieldDescription {
2 |
3 | @AuraEnabled
4 | public Boolean autoNumber { get; set; }
5 | @AuraEnabled
6 | public Integer byteLength { get; set; }
7 | @AuraEnabled
8 | public Boolean calculated { get; set; }
9 | @AuraEnabled
10 | public String calculatedFormula { get; set; }
11 | @AuraEnabled
12 | public Boolean cascadeDelete { get; set; }
13 | @AuraEnabled
14 | public Boolean caseSensitive { get; set; }
15 | @AuraEnabled
16 | public String controllerName { get; set; }
17 | @AuraEnabled
18 | public Boolean createable { get; set; }
19 | @AuraEnabled
20 | public Boolean custom { get; set; }
21 | @AuraEnabled
22 | public Object defaultValue { get; set; }
23 | @AuraEnabled
24 | public String defaultValueFormula { get; set; }
25 | @AuraEnabled
26 | public Boolean defaultedOnCreate { get; set; }
27 | @AuraEnabled
28 | public Boolean dependentPicklist { get; set; }
29 | @AuraEnabled
30 | public Boolean deprecatedAndHidden { get; set; }
31 | @AuraEnabled
32 | public Integer digits { get; set; }
33 | @AuraEnabled
34 | public Boolean displayLocationInDecimal { get; set; }
35 | @AuraEnabled
36 | public Boolean externalId { get; set; }
37 | @AuraEnabled
38 | public String extraTypeInfo { get; set; }
39 | @AuraEnabled
40 | public Boolean filterable { get; set; }
41 | @AuraEnabled
42 | public List