Skip to content

Commit

Permalink
Include child relationship names on sobject export
Browse files Browse the repository at this point in the history
Fetch all related sobject metadata to obtain the child relationship name for any lookup fields when the user chooses the option to include this.

There are no other SOQL based ways to obtain this data, so we end up needing to take the performance hit of fetching metadata for all related sobjects

resolves #1102
  • Loading branch information
paustint committed Dec 8, 2024
1 parent 045d620 commit 6e1ab45
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 31 deletions.
28 changes: 19 additions & 9 deletions libs/features/sobject-export/src/SObjectExport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,25 @@ import { applicationCookieState, fromJetstreamEvents, selectedOrgState } from '@
import localforage from 'localforage';
import { Fragment, FunctionComponent, useEffect, useRef, useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { ExportHeaderOption, ExportOptions, ExportWorksheetLayout, SavedExportOptions } from './sobject-export-types';
import { getAttributes, getSobjectMetadata, prepareExport } from './sobject-export-utils';
import {
ExportHeaderOption,
ExportOptions,
ExportWorksheetLayout,
SavedExportOptions,
SobjectExportFieldName,
} from './sobject-export-types';
import { getAttributes, getChildRelationshipNames, getSobjectMetadata, prepareExport } from './sobject-export-utils';

const HEIGHT_BUFFER = 170;
const FIELD_ATTRIBUTES: ListItem[] = getAttributes().map(({ label, name, description }) => ({
const FIELD_ATTRIBUTES: ListItem<SobjectExportFieldName>[] = getAttributes().map(({ label, name, description, tertiaryLabel }) => ({
id: name,
label: `${label} (${name})`,
value: name,
secondaryLabel: description,
tertiaryLabel,
}));

const DEFAULT_SELECTION = [
const DEFAULT_SELECTION: SobjectExportFieldName[] = [
'calculatedFormula',
'createable',
'custom',
Expand Down Expand Up @@ -81,7 +88,7 @@ export const SObjectExport: FunctionComponent<SObjectExportProps> = () => {

const [sobjects, setSobjects] = useState<Maybe<DescribeGlobalSObjectResult[]>>();
const [selectedSObjects, setSelectedSObjects] = useState<string[]>([]);
const [selectedAttributes, setSelectedAttributes] = useState<string[]>([]);
const [selectedAttributes, setSelectedAttributes] = useState<SobjectExportFieldName[]>([]);

const [exportDataModalOpen, setExportDataModalOpen] = useState<boolean>(false);
const [exportDataModalData, setExportDataModalData] = useState<Record<string, any[]>>({});
Expand All @@ -108,7 +115,7 @@ export const SObjectExport: FunctionComponent<SObjectExportProps> = () => {
}
}
if (results?.fields) {
setSelectedAttributes(results.fields);
setSelectedAttributes(results.fields as SobjectExportFieldName[]);
} else {
setSelectedAttributes([...DEFAULT_SELECTION]);
}
Expand Down Expand Up @@ -149,7 +156,10 @@ export const SObjectExport: FunctionComponent<SObjectExportProps> = () => {
setLoading(true);
setErrorMessage(null);
const metadataResults = await getSobjectMetadata(selectedOrg, selectedSObjects);
const output = prepareExport(metadataResults, selectedAttributes, options);
const sobjectsWithChildRelationships = selectedAttributes.includes('childRelationshipName')
? await getChildRelationshipNames(selectedOrg, metadataResults)
: {};
const output = prepareExport(metadataResults, sobjectsWithChildRelationships, selectedAttributes, options);

if (options.saveAsDefaultSelection) {
try {
Expand Down Expand Up @@ -248,12 +258,12 @@ export const SObjectExport: FunctionComponent<SObjectExportProps> = () => {
descriptorSingular: 'field attribute',
descriptorPlural: 'field attributes',
}}
items={FIELD_ATTRIBUTES}
items={FIELD_ATTRIBUTES as ListItem[]}
selectedItems={selectedAttributes}
allowRefresh
lastRefreshed="Reset to default"
onRefresh={resetAttributesToDefault}
onSelected={setSelectedAttributes}
onSelected={(items) => setSelectedAttributes(items as SobjectExportFieldName[])}
/>
</div>
<div className="slds-p-horizontal_x-small">
Expand Down
5 changes: 4 additions & 1 deletion libs/features/sobject-export/src/sobject-export-types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { DescribeSObjectResult, Field, Maybe } from '@jetstream/types';
import { ChildRelationship, DescribeSObjectResult, Field, Maybe } from '@jetstream/types';

export type SobjectExportFieldName =
| keyof Field
| 'childRelationshipName'
| 'dataTranslationEnabled'
| 'autoNumber'
| 'aiPredictionField'
Expand All @@ -13,7 +14,9 @@ export interface SobjectExportField {
name: SobjectExportFieldName;
label: string;
description?: string;
tertiaryLabel?: string;
getterFn?: (value: any) => string;
childRelationshipGetterFn?: (field: Field, sobjectsWithChildRelationships: Record<string, Record<string, ChildRelationship>>) => string;
}

export interface SavedExportOptions {
Expand Down
77 changes: 70 additions & 7 deletions libs/features/sobject-export/src/sobject-export-utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { logger } from '@jetstream/shared/client-logger';
import { describeSObject } from '@jetstream/shared/data';
import { splitArrayToMaxSize } from '@jetstream/shared/utils';
import { ApiResponse, DescribeSObjectResult, SalesforceOrgUi } from '@jetstream/types';
import { logErrorToRollbar } from '@jetstream/shared/ui-utils';
import { getErrorMessageAndStackObj, splitArrayToMaxSize } from '@jetstream/shared/utils';
import { ApiResponse, ChildRelationship, DescribeSObjectResult, Field, SalesforceOrgUi } from '@jetstream/types';
import isFunction from 'lodash/isFunction';
import isString from 'lodash/isString';
import { ExportOptions, SobjectExportField, SobjectFetchResult } from './sobject-export-types';
Expand Down Expand Up @@ -39,8 +41,46 @@ export async function getSobjectMetadata(org: SalesforceOrgUi, selectedSobjects:
});
}

export async function getChildRelationshipNames(
selectedOrg: SalesforceOrgUi,
metadataResults: SobjectFetchResult[]
): Promise<Record<string, Record<string, ChildRelationship>>> {
try {
// Get Parent SObject names from all relationship fields and remove duplicates
const relatedSobjects = Array.from(
new Set(
metadataResults.flatMap(
(item) =>
item.metadata?.fields
.filter((field) => field.type === 'reference' && field.referenceTo?.length === 1)
.flatMap((field) => field.referenceTo || []) || []
)
)
);
// Fetch all parent sobject metadata (hopefully from cache for many of them) and reduce into map for easy lookup
const sobjectsWithChildRelationships = await getSobjectMetadata(selectedOrg, relatedSobjects).then((results) =>
results.reduce((sobjectsWithChildRelationships: Record<string, Record<string, ChildRelationship>>, { metadata, sobject }) => {
sobjectsWithChildRelationships[sobject] = (metadata?.childRelationships || []).reduce(
(acc: Record<string, ChildRelationship>, childRelationship) => {
acc[childRelationship.field] = childRelationship;
return acc;
},
{}
);
return sobjectsWithChildRelationships;
}, {})
);
return sobjectsWithChildRelationships;
} catch (ex) {
logger.warn('Error getting child relationship names for sobject export', ex);
logErrorToRollbar('Error getting child relationship names for sobject export', getErrorMessageAndStackObj(ex));
return {};
}
}

export function prepareExport(
sobjectMetadata: SobjectFetchResult[],
sobjectsWithChildRelationships: Record<string, Record<string, ChildRelationship>>,
selectedAttributes: string[],
options: ExportOptions
): Record<string, any[]> {
Expand All @@ -62,16 +102,18 @@ export function prepareExport(
rowsBySobject[sobject] =
metadata?.fields
.filter((field) => (options.includesStandardFields ? true : field.custom))
.flatMap((field: any) => {
.flatMap((field: Field) => {
const obj = { 'Object Name': sobject } as any;
selectedAttributeFields.forEach(({ name, label, getterFn }) => {
selectedAttributeFields.forEach(({ name, label, getterFn, childRelationshipGetterFn: relationshipGetterFn }) => {
const _label = options.headerOption === 'label' ? label : name;
// TODO: transform as required
const value = field[name as keyof Field];

if (isFunction(getterFn)) {
obj[_label] = getterFn(field[name]);
obj[_label] = getterFn(value);
} else if (isFunction(relationshipGetterFn)) {
obj[_label] = relationshipGetterFn(field, sobjectsWithChildRelationships);
} else {
obj[_label] = field[name];
obj[_label] = value;
}
});
return obj;
Expand Down Expand Up @@ -192,6 +234,27 @@ export function getAttributes(): SobjectExportField[] {
label: 'Calculated Formula',
description: 'Formula definition. Only populated if field type is Formula.',
},
{
name: 'childRelationshipName',
label: 'Child Relationship Name',
description: 'Child relationship name(s) for lookup field.',
childRelationshipGetterFn: (field: Field, sobjectsWithChildRelationships: Record<string, Record<string, ChildRelationship>>) => {
const relatedSObjects = field.referenceTo || [];
if (relatedSObjects.length === 0) {
return '';
}
return relatedSObjects
.map((relatedSObject) => {
const childRelationship = sobjectsWithChildRelationships[relatedSObject]?.[field.name]?.relationshipName;
if (childRelationship) {
return childRelationship;
}
return null;
})
.filter(Boolean)
.join(', ');
},
},
{
name: 'controllerName',
label: 'Controller Name',
Expand Down
18 changes: 4 additions & 14 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -11546,20 +11546,10 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"

caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001335, caniuse-lite@^1.0.30001349, caniuse-lite@^1.0.30001370, caniuse-lite@^1.0.30001400, caniuse-lite@^1.0.30001426, caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001503, caniuse-lite@^1.0.30001541, caniuse-lite@^1.0.30001565, caniuse-lite@^1.0.30001587:
version "1.0.30001636"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz"
integrity sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==

caniuse-lite@^1.0.30001579:
version "1.0.30001639"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001639.tgz#972b3a6adeacdd8f46af5fc7f771e9639f6c1521"
integrity sha512-eFHflNTBIlFwP2AIKaYuBQN/apnUoKNhBdza8ZnW/h2di4LCZ4xFqYlxUxo+LQ76KFI1PGcC1QDxMbxTZpSCAg==

caniuse-lite@^1.0.30001646:
version "1.0.30001657"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001657.tgz#29fd504bffca719d1c6b63a1f6f840be1973a660"
integrity sha512-DPbJAlP8/BAXy3IgiWmZKItubb3TYGP0WscQQlVGIfT4s/YlFYVuJgyOsQNP7rJRChx/qdMeLJQJP0Sgg2yjNA==
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001335, caniuse-lite@^1.0.30001349, caniuse-lite@^1.0.30001370, caniuse-lite@^1.0.30001400, caniuse-lite@^1.0.30001426, caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001503, caniuse-lite@^1.0.30001541, caniuse-lite@^1.0.30001565, caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001646:
version "1.0.30001687"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001687.tgz"
integrity sha512-0S/FDhf4ZiqrTUiQ39dKeUjYRjkv7lOZU1Dgif2rIqrTzX/1wV2hfKu9TOm1IHkdSijfLswxTFzl/cvir+SLSQ==

caseless@~0.12.0:
version "0.12.0"
Expand Down

0 comments on commit 6e1ab45

Please sign in to comment.