Skip to content

Commit

Permalink
✨ Check Salesforce package version (#131)
Browse files Browse the repository at this point in the history
* Add query the package versions installed in the Salesforce organization to check for dependencies.
  • Loading branch information
shunkosa authored Feb 27, 2024
1 parent 4f36fab commit 5f89f55
Show file tree
Hide file tree
Showing 15 changed files with 131 additions and 67 deletions.
6 changes: 3 additions & 3 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@
],
"ios": {
"bundleIdentifier": "com.sfdo.community.grassrootsmobilesurveyapp",
"buildNumber": "9"
"buildNumber": "11"
},
"android": {
"package": "com.sfdo.community.grassrootsmobilesurveyapp",
"versionCode": 2,
"versionCode": 4,
"adaptiveIcon": {
"backgroundColor": "#ffffff",
"foregroundImage": "./assets/images/foreground.png"
},
"permissions": []
},
"version": "0.12.0",
"version": "0.13.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"splash": {
Expand Down
2 changes: 1 addition & 1 deletion src/components/login/Welcome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Modal from 'react-native-modal';

import { getCurrentFieldWorker, storeContacts } from '../../services/salesforce/contact';
import { storeOnlineSurveys } from '../../services/salesforce/survey';
import { retrieveAllMetadata } from '../../services/describe';
import { retrieveAllMetadata } from '../../services/allMetadata';
import { clearLocal } from '../../services/session';
import { useLocalizationContext } from '../../context/localizationContext';
import { useAuthContext } from '../../context/authContext';
Expand Down
4 changes: 2 additions & 2 deletions src/config/locale/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
"LOGIN": "Log In",
"LOGIN_SETTINGS": "Log In Settings",
"ERROR": "Error",
"ERROR_ON_SAVE": "An error occured while saving data. Please try again.",
"ERROR_ON_SAVE": "An error occurred while saving data. Please try again.",
"LOGOUT": "Logout",
"SELECT": "Select",
"LOGOUT_MESSAGE": "Do you really want to logout from app? Unsynced surverys will disappear.",
"LOGOUT_MESSAGE": "Do you really want to logout from app? Unsynced surveys will disappear.",
"SURVEY": "Survey",
"SURVEY_DETAIL": "Survey Detail",
"SURVEYS": "Surveys",
Expand Down
9 changes: 9 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ export const LOCAL_SURVEY_FIELDS: Array<SQLiteFieldTypeMapping> = [{ name: '_syn
export const SYNC_STATUS_SYNCED = 'Synced';
export const SYNC_STATUS_UNSYNCED = 'Unsynced';

export const SUBSCRIBER_PACKAGE_VERSION = '0336g000000kHHoAAM';
export const MIN_PACKAGE_VERSION = '0.4.0.5';

export const DB_TABLE = {
RECORD_TYPE: 'RecordType',
PAGE_LAYOUT_SECTION: 'PageLayoutSection',
Expand Down Expand Up @@ -110,3 +113,9 @@ export const SUPPORTED_SF_LANGUAGES: Array<Language> = [
{ name: 'Thai', code: 'th' },
{ name: 'Nepali', code: 'ne' }, // For HaydenHall ❤️🇳🇵
];

export const METADATA_ERROR = {
INVALID_PACKAGE_VERSION: 'INVALID_PACKAGE_VERSION',
NO_EDITABLE_FIELDS: 'NO_EDITABLE_FIELDS',
INVALID_RECORD_TYPE: 'INVALID_RECORD_TYPE',
};
2 changes: 1 addition & 1 deletion src/screens/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as Application from 'expo-application';
import * as SecureStore from 'expo-secure-store';

import { useLocalizationContext } from '../context/localizationContext';
import { retrieveAllMetadata } from '../services/describe';
import { retrieveAllMetadata } from '../services/allMetadata';
import { forceLogout } from '../services/session';
import { Loader } from '../components';

Expand Down
2 changes: 1 addition & 1 deletion src/screens/SurveyEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { useLocalizationContext } from '../context/localizationContext';
import { useSelector, useDispatch } from '../context/surveyEditorContext';
// services
import { getRecords } from '../services/database/database';
import { buildLayoutDetail } from '../services/describe';
import { buildLayoutDetail } from '../services/layout';
import { notifyError, notifySuccess } from '../utility/notification';
import { upsertLocalSurvey } from '../services/database/localSurvey';
// constants
Expand Down
67 changes: 14 additions & 53 deletions src/services/describe.ts → src/services/allMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,22 @@ import {
storePageLayoutSections,
storeLocalization,
} from './salesforce/metadata';
import { saveRecords, getRecords, clearTable } from './database/database';
import { SQLitePageLayoutSection, SQLitePageLayoutItem, SQLitePicklistValue } from '../types/sqlite';
import { SurveyLayout } from '../types/survey';
import { saveRecords, clearTable } from './database/database';
import { SQLitePicklistValue } from '../types/sqlite';
import { CompositeLayoutResponse, DescribeLayout } from '../types/metadata';

import { logger } from '../utility/logger';
import { ASYNC_STORAGE_KEYS, DB_TABLE, SURVEY_OBJECT } from '../constants';
import { ASYNC_STORAGE_KEYS, DB_TABLE, METADATA_ERROR, MIN_PACKAGE_VERSION, SURVEY_OBJECT } from '../constants';
import { describeLayouts } from './salesforce/core';
import { validateInstalledPackageVersion } from './salesforce/installedPackage';

/**
* @description Download record types, all the page layouts, and localization custom metadata.
*/
export const retrieveAllMetadata = async () => {
try {
// Store package version
await validateInstalledPackageVersion();
// Record types with compact layout title
await clearTable(DB_TABLE.RECORD_TYPE);
const recordTypes = await storeRecordTypesWithCompactLayout();
Expand Down Expand Up @@ -56,56 +58,15 @@ export const retrieveAllMetadata = async () => {
await storeLocalization();
} catch (e) {
logger('ERROR', 'retrieveAllMetadata', e);
if (e.error === 'invalid_record_type') {
throw new Error('Invalid record type on Survey object. Contact your administrator.');
} else if (e.error === 'no_editable_fields') {
if (e.error === METADATA_ERROR.INVALID_PACKAGE_VERSION) {
throw new Error(
`Salesforce package is not installed or is old. Install at least version ${MIN_PACKAGE_VERSION}.`
);
} else if (e.error === METADATA_ERROR.INVALID_RECORD_TYPE) {
throw new Error('Invalid record type found on Survey object. Contact your administrator.');
} else if (e.error === METADATA_ERROR.NO_EDITABLE_FIELDS) {
throw new Error('No editable fields on Survey layout. Contact your administrator.');
}
throw new Error('Unexpected error occured while retrieving survey settings. Contact your administrator.');
throw new Error('Unexpected error occurred while retrieving survey settings. Contact your administrator.');
}
};

/**
* Construct page layout object from locally stored page layout sections and items
* @param layoutId
*/
export const buildLayoutDetail = async (layoutId: string): Promise<SurveyLayout> => {
// sections in the layout
const sections: Array<SQLitePageLayoutSection> = await getRecords(
DB_TABLE.PAGE_LAYOUT_SECTION,
`where layoutId='${layoutId}'`
);
// items used in the sections
const sectionIds = sections.map(s => s.id);
const items: Array<SQLitePageLayoutItem> = await getRecords(
DB_TABLE.PAGE_LAYOUT_ITEM,
`where sectionId in (${sectionIds.map(id => `'${id}'`).join(',')})`
);
logger('FINE', 'buildLayoutDetail', items);

// group items by section id
const sectionIdToItems = items.reduce(
(result, item) => ({
...result,
[item.sectionId]: [...(result[item.sectionId] || []), item],
}),
{}
);

const layout: SurveyLayout = {
sections: sections.map(s => ({
id: s.id,
title: s.sectionLabel,
data: sectionIdToItems[s.id]
? sectionIdToItems[s.id].map(item => ({
name: item.fieldName,
label: item.fieldLabel,
type: item.fieldType,
required: !!item.required,
}))
: [],
})),
};

return layout;
};
50 changes: 50 additions & 0 deletions src/services/layout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { getRecords } from './database/database';
import { SQLitePageLayoutSection, SQLitePageLayoutItem } from '../types/sqlite';
import { SurveyLayout } from '../types/survey';
import { logger } from '../utility/logger';
import { DB_TABLE } from '../constants';

/**
* Construct page layout object from locally stored page layout sections and items
* @param layoutId
*/
export const buildLayoutDetail = async (layoutId: string): Promise<SurveyLayout> => {
// sections in the layout
const sections: Array<SQLitePageLayoutSection> = await getRecords(
DB_TABLE.PAGE_LAYOUT_SECTION,
`where layoutId='${layoutId}'`
);
// items used in the sections
const sectionIds = sections.map(s => s.id);
const items: Array<SQLitePageLayoutItem> = await getRecords(
DB_TABLE.PAGE_LAYOUT_ITEM,
`where sectionId in (${sectionIds.map(id => `'${id}'`).join(',')})`
);
logger('FINE', 'buildLayoutDetail', items);

// group items by section id
const sectionIdToItems = items.reduce(
(result, item) => ({
...result,
[item.sectionId]: [...(result[item.sectionId] || []), item],
}),
{}
);

const layout: SurveyLayout = {
sections: sections.map(s => ({
id: s.id,
title: s.sectionLabel,
data: sectionIdToItems[s.id]
? sectionIdToItems[s.id].map(item => ({
name: item.fieldName,
label: item.fieldLabel,
type: item.fieldType,
required: !!item.required,
}))
: [],
})),
};

return layout;
};
22 changes: 22 additions & 0 deletions src/services/salesforce/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,28 @@ export const fetchSalesforceRecords = async (query: string) => {
});
};

/**
* @description Execute SOQL and return records
* @param query SOQL
* @return records
*/
export const fetchToolingRecords = async (query: string) => {
const endPoint = (await buildEndpointUrl()) + `/tooling/query?q=${query}`;
const response = await fetchRetriable({ endPoint, method: 'GET', body: undefined });
// Error response of Salesforce REST API is array format.
const hasError = Array.isArray(response);
if (hasError) {
logger('ERROR', 'fetchToolingRecords', response);
return Promise.reject({ origin: 'query' });
}
return response.totalSize === 0
? []
: response.records.map(r => {
delete r.attributes;
return r;
});
};

/**
* @description Retrieve record details using composite resource
* @param sObjectType
Expand Down
16 changes: 16 additions & 0 deletions src/services/salesforce/installedPackage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { METADATA_ERROR, MIN_PACKAGE_VERSION, SUBSCRIBER_PACKAGE_VERSION } from '../../constants';
import { fetchToolingRecords } from './core';

export const validateInstalledPackageVersion = async () => {
const query = `SELECT SubscriberPackageVersion.MajorVersion,SubscriberPackageVersion.MinorVersion, SubscriberPackageVersion.PatchVersion, SubscriberPackageVersion.BuildNumber FROM InstalledSubscriberPackage WHERE SubscriberPackageId = '${SUBSCRIBER_PACKAGE_VERSION}'`;
const records = await fetchToolingRecords(query);
if (records.length === 0) {
return Promise.reject({ error: METADATA_ERROR.INVALID_PACKAGE_VERSION });
}
const record = records[0].SubscriberPackageVersion;
const version = `${record.MajorVersion}.${record.MinorVersion}.${record.PatchVersion}.${record.BuildNumber}`;
if (version < MIN_PACKAGE_VERSION) {
return Promise.reject({ error: METADATA_ERROR.INVALID_PACKAGE_VERSION });
}
return Promise.resolve({});
};
5 changes: 3 additions & 2 deletions src/services/salesforce/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
SUPPORTED_SF_LANGUAGES,
DEFAULT_SF_LANGUAGE,
AVAILABLE_LANGUAGE_CMDT,
METADATA_ERROR,
} from '../../constants';

/**
Expand All @@ -39,7 +40,7 @@ export const storeRecordTypesWithCompactLayout = async () => {
response.recordTypeMappings[0].developerName === 'Master' &&
response.recordTypeMappings[0].master === false
) {
return Promise.reject({ error: 'invalid_record_type' });
return Promise.reject({ error: METADATA_ERROR.INVALID_RECORD_TYPE });
}
const recordTypes: Array<SQLiteRawRecordType> = response.recordTypeMappings.map(r => ({
developerName: r.developerName,
Expand Down Expand Up @@ -124,7 +125,7 @@ export const storePageLayoutItems = async (layout: DescribeLayout) => {
.flat(3);
logger('FINE', 'storePageLayoutItems | items', pageLayoutItems);
if (pageLayoutItems.length === 0) {
return Promise.reject({ error: 'no_editable_fields' });
return Promise.reject({ error: METADATA_ERROR.NO_EDITABLE_FIELDS });
}
await saveRecords(DB_TABLE.PAGE_LAYOUT_ITEM, pageLayoutItems, undefined);

Expand Down
2 changes: 1 addition & 1 deletion src/services/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const syncLocalSurveys = async (localSurveys: Array<any>) => {
} else if (response.hasErrors) {
throw new Error(`Upload failed: ${response.results[0].errors[0].message}`);
} else {
throw new Error('Unexpected error occued while uploading. Contact your adminsitrator.');
throw new Error('Unexpected error occurred while uploading. Contact your administrator.');
}
} catch (e) {
notifyError(e.message);
Expand Down
7 changes: 6 additions & 1 deletion src/utility/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ export function logger(loggingLevel: LoggingLevel, name: string, message: string
return;
}
const formattedLoggingLevel = `[${loggingLevel}]`.padEnd(7, ' ');
const formattedMessage = typeof message === 'object' ? '\n' + JSON.stringify(message, undefined, ' ') : message;
const formattedMessage =
message instanceof Error
? JSON.stringify(message, Object.getOwnPropertyNames(message))
: typeof message === 'object'
? '\n' + JSON.stringify(message, undefined, ' ')
: message;
console.log(`${formattedLoggingLevel} ${new Date().toString()} | ${name} | ${formattedMessage}`);
}
2 changes: 1 addition & 1 deletion tests/screens/Login.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jest.mock('../../src/services/salesforce/survey', () => ({
storeOnlineSurveys: jest.fn().mockImplementation(() => Promise.resolve()),
}));

jest.mock('../../src/services/describe', () => ({
jest.mock('../../src/services/allMetadata', () => ({
retrieveAllMetadata: jest.fn().mockImplementation(() => Promise.resolve()),
}));

Expand Down
2 changes: 1 addition & 1 deletion tests/services/describe.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { buildLayoutDetail } from '../../src/services/describe';
import { buildLayoutDetail } from '../../src/services/layout';
import { getRecords } from '../../src/services/database/database';

jest.mock('../../src/services/database/database', () => ({
Expand Down

0 comments on commit 5f89f55

Please sign in to comment.