Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update metadata to API v61, add Apex Docs to code, improve help text and descriptions. #18

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,28 @@ A flexible cache management system for Salesforce Apex developers. Built to be s

Learn more about the history & implementation of this repo in [the Joys of Apex article 'Iteratively Building a Flexible Caching System for Apex'](https://www.jamessimone.net/blog/joys-of-apex/iteratively-building-a-flexible-caching-system/)

## Unlocked Package - `Nebula` Namespace - v1.0.2
## Unlocked Package - `Nebula` Namespace - v1.0.3

[![Install Unlocked Package (Nebula namespace) in a Sandbox](./images/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015nCfQAI)
[![Install Unlocked Package (Nebula namespace) in Production](./images/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015nCfQAI)

## Unlocked Package - No Namespace - v1.0.2
## Unlocked Package - No Namespace - v1.0.3

[![Install Unlocked Package (no namespace) in a Sandbox](./images/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015nCaQAI)
[![Install Unlocked Package (no namespace) in Production](./images/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015nCaQAI)

---
## Required setup after installation
Go to Setup -> Custom Code -> Platform Cache. Click on `CacheManagerPartition` then click Edit button set at least 1 MB for "Session Cache Allocation" and 1 MB "Org Cache Allocation".
This is needed as otherwise cache won't work with default 0 values.

If you want to debug and see what is stored in cache go to your user profile and check "Cache Diagnostics" checkbox, then save.
Now if you go again to Platform Cache and click `CacheManagerPartition` you will see additional field with link arrow named "Diagnostics". When clicked it opens teh page with all details and statistics about cache.

## Optional setup
In Custom metadata you will see `Cache Configuration` and `Cache Value` metadata types.
In `Cache Configuration` you can adjust if cache type is enabled or disabled, set it as immutable, set cache time to live value and define if it is global or namespace scoped cache.
In `Cache Value` you can define persistent values which should be always available in cache.

## Cache Manager for Apex: Quick Start

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xml version="1.0" encoding="UTF-8"?>
<PlatformCachePartition xmlns="http://soap.sforce.com/2006/04/metadata">
<description>Platform cache partition used by Nebula Cache Manager.
When a partition has no allocation, cache operations (such as get and put) are not invoked, and no error is returned. Set minimum 1 MB for each type.</description>
<isDefaultPartition>false</isDefaultPartition>
<masterLabel>CacheManagerPartition</masterLabel>
<platformCachePartitionTypes>
Expand Down
161 changes: 149 additions & 12 deletions nebula-cache-manager/core/classes/CacheManager.cls
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,28 @@
// See LICENSE file or go to https://github.com/jongpie/NebulaCacheManager for full license details. //
//---------------------------------------------------------------------------------------------------//

@SuppressWarnings('PMD.ApexDoc, PMD.AvoidDebugStatements, PMD.AvoidGlobalModifier, PMD.ExcessivePublicCount, PMD.PropertyNamingConventions')
@SuppressWarnings('PMD.ApexDoc, PMD.AvoidDebugStatements, PMD.AvoidGlobalModifier, PMD.ExcessivePublicCount, PMD.PropertyNamingConventions, PMD.CognitiveComplexity')
global without sharing class CacheManager {
@TestVisible
private static final Map<String, Cacheable> CONFIGURATION_DEVELOPER_NAME_TO_CACHEABLE_INSTANCE = new Map<String, Cacheable>();
@TestVisible
private static final List<CacheValue__mdt> CONFIGURED_CACHE_VALUES = Schema.CacheValue__mdt.getAll().values();
@TestVisible
private static final String CURRENT_VERSION_NUMBER = 'v1.0.2';
private static final String CURRENT_VERSION_NUMBER = 'v1.0.3';
@TestVisible
private static final String PLATFORM_CACHE_NULL_VALUE = '<{(CACHE_VALUE_IS_NULL)}>'; // Presumably, no one will ever use this as an actual value

// Load predefined cache values from "Cache Value" custom metadata. They will be always available in cache.
@TestVisible
private static final List<CacheValue__mdt> CONFIGURED_CACHE_VALUES = Schema.CacheValue__mdt.getAll().values();

// Load partitions configurations from "Cache Configuration" custom metadata.
@TestVisible
private static final CacheConfiguration__mdt ORGANIZATION_CACHE_CONFIGURATION = Schema.CacheConfiguration__mdt.getInstance('Organization').clone();
@TestVisible
private static final CacheConfiguration__mdt SESSION_CACHE_CONFIGURATION = Schema.CacheConfiguration__mdt.getInstance('Session').clone();
@TestVisible
private static final CacheConfiguration__mdt TRANSACTION_CACHE_CONFIGURATION = Schema.CacheConfiguration__mdt.getInstance('Transaction').clone();

private static Map<PlatformCacheType, PlatformCachePartitionProxy> cacheTypeToMockPartitionProxy = new Map<PlatformCacheType, PlatformCachePartitionProxy>();
private static Map<PlatformCachePartitionType, PlatformCachePartitionProxy> cacheTypeToMockPartitionProxy = new Map<PlatformCachePartitionType, PlatformCachePartitionProxy>();

private static final System.Pattern ALPHANUMERIC_REGEX_PATTERN {
get {
Expand All @@ -37,46 +41,162 @@ global without sharing class CacheManager {
}

@TestVisible
private enum PlatformCacheType {
private enum PlatformCachePartitionType {
ORGANIZATION,
SESSION
}

/**
* @description Interface used to define caches that can be used to store values via different mechanisms
*/
global interface Cacheable {
/**
* @description Indicates if the specified key has already been added to the cache
* @param key The `String` key to check for within the cache
* @return The `Boolean` result that indicates if the specified key is contained in the cache
*/
Boolean contains(String key);

/**
* @description Indicates if the specified keys have already been added to the cache
* @param keys The Set of `String` keys to check for within the cache
* @return The `Map<String, Boolean>` result that indicates if the specified key is contained in the cache
*/
Map<String, Boolean> contains(Set<String> keys);

/**
* @description Indicates if the specified keys have already been added to the cache
* @param keys The Set of `String` keys to check for within the cache
* @return The `Boolean` result that indicates if all given keys are contained in the cache
*/
Boolean containsAll(Set<String> keys);

/**
* @description Returns the cached value for the specified key, or `null` if the specified key does not exist in the cache
* @param key The `String` key to check for within the cache
* @return The `Object` cached value, or `null` if no cached value is found for the specified key
*/
Object get(String key);

/**
* @description Returns the cached value for the specified key, or `null` if the specified key does not exist in the cache
* @param key The `String` key to check for within the cache
* @param cacheBuilderClass Instance of cacheBuilderClass
* @return The cached value, or `null` if no cached value is found for the specified key
*/
Object get(String key, System.Type cacheBuilderClass);

/**
* @description Returns the cached values for the specified keys, or `null` if the specified key does not exist in the cache
* @param keys The Set of `String` keys to check for within the cache
* @return The `Map<String, Object>` with cached value, or `null` if no cached value is found for the specified key
*/
Map<String, Object> get(Set<String> keys);

/**
* @description Returns all cached values
* @return The `Map<String, Object>` with all cached values
*/
Map<String, Object> getAll();

/**
* @description Returns all Keys stored in cache.
* @return `Set<String>` with all keys of cache entries.
*/
Set<String> getKeys();

/**
* @description Check if cache is Available for use (enabled and configured)
* @return `Boolean`
*/
Boolean isAvailable();

/**
* @description Check if cache was configured in Custom Metadata as Enabled.
* @return `Boolean`
*/
Boolean isEnabled();

/**
* @description Check if cache was configured in Custom Metadata as Immutable.
* @return `Boolean`
*/
Boolean isImmutable();

/**
* @description Adds the provided `Object` value to the cache, using the specified `String` key
* @param key The `String` key to add to the cache
* @param value The `Object` value to cache for the specified key
*/
void put(String key, Object value);

/**
* @description Adds the provided `Object` values to the cache, using the specified `String` keys.
* @param keyToValue The Map of `String` keys to add to the cache with the `Object` values for that keys.
*/
void put(Map<String, Object> keyToValue);

/**
* @description Removes the specified `String` key from the cache
* @param key The `String` key to remove from the cache
*/
void remove(String key);

/**
* @description Removes the specified Set of `String` keys from the cache
* @param keys The Set of `String` keys to remove from the cache
*/
void remove(Set<String> keys);

/**
* @description Removes all keys from cache. Clear cache.
*/
void removeAll();
}

/**
* @description The instance of `Cacheable` used for any organization-specific caching via Platform Cache.
* When Platform Cache is disabled or not available, the transaction cache is instead used.
* @return The singleton instance of `Cacheable`
*/
global static Cacheable getOrganizationCache() {
return getOrganizationCache(ORGANIZATION_CACHE_CONFIGURATION);
}

/**
* @description The instance of `Cacheable` used for any organization-specific caching via Platform Cache.
* When Platform Cache is disabled or not available, the transaction cache is instead used.
* @param configuration Configuration stored in Custom Metadata record for Organization Cache
* @return The singleton instance of `Cacheable`
*/
global static Cacheable getOrganizationCache(CacheConfiguration__mdt configuration) {
return getPlatformCache(configuration, PlatformCacheType.ORGANIZATION);
return getPlatformCache(configuration, PlatformCachePartitionType.ORGANIZATION);
}

/**
* @description The instance of `Cacheable` used for any session-specific caching via Platform Cache.
* When Platform Cache is disabled or not available, the transaction cache is instead used.
* @return The singleton instance of `Cacheable`
*/
global static Cacheable getSessionCache() {
return getSessionCache(SESSION_CACHE_CONFIGURATION);
}

/**
* @description The instance of `Cacheable` used for any organization-specific caching via Platform Cache.
* When Platform Cache is disabled or not available, the transaction cache is instead used.
* @param configuration Configuration stored in Custom Metadata record for Session Cache
* @return The singleton instance of `Cacheable`
*/
global static Cacheable getSessionCache(CacheConfiguration__mdt configuration) {
return getPlatformCache(configuration, PlatformCacheType.SESSION);
return getPlatformCache(configuration, PlatformCachePartitionType.SESSION);
}

/**
* @description The instance of `Cacheable` used for any transaction-specific caching.
* Cached data is stored internally in-memory for the duration of the transaction.
* @return The singleton instance of `Cacheable`
*/
global static Cacheable getTransactionCache() {
if (CONFIGURATION_DEVELOPER_NAME_TO_CACHEABLE_INSTANCE.containsKey(TRANSACTION_CACHE_CONFIGURATION.DeveloperName)) {
return CONFIGURATION_DEVELOPER_NAME_TO_CACHEABLE_INSTANCE.get(TRANSACTION_CACHE_CONFIGURATION.DeveloperName);
Expand All @@ -89,10 +209,15 @@ global without sharing class CacheManager {
}

@TestVisible
private static void setMockPartitionProxy(PlatformCacheType cacheType, PlatformCachePartitionProxy mockPartitionProxy) {
private static void setMockPartitionProxy(PlatformCachePartitionType cacheType, PlatformCachePartitionProxy mockPartitionProxy) {
cacheTypeToMockPartitionProxy.put(cacheType, mockPartitionProxy);
}

/**
* @description Validate if given key is alphanumeric without any special characters. Allowed characters a-z A-Z 0-9
* @param key String to be checked.
* @exception Exception is thrown if key contains not allowed characters.
*/
@TestVisible
private static void validateKey(String key) {
Matcher regexMatcher = ALPHANUMERIC_REGEX_PATTERN.matcher(key);
Expand All @@ -101,7 +226,7 @@ global without sharing class CacheManager {
}
}

private static Cacheable getPlatformCache(CacheConfiguration__mdt configuration, PlatformCacheType cacheType) {
private static Cacheable getPlatformCache(CacheConfiguration__mdt configuration, PlatformCachePartitionType cacheType) {
if (CONFIGURATION_DEVELOPER_NAME_TO_CACHEABLE_INSTANCE.containsKey(configuration.DeveloperName)) {
return CONFIGURATION_DEVELOPER_NAME_TO_CACHEABLE_INSTANCE.get(configuration.DeveloperName);
}
Expand All @@ -122,6 +247,12 @@ global without sharing class CacheManager {
return platformCache;
}

/**
* @description Check if given cache type is enabled and load permanent cache values from "Cache Value" custom metadata.
* Values are loaded for given type of Cache and only if they are enabled.
* @param cacheConfiguration Custom Metadata record
* @return `Map<String, Object>` Map of key to cache value.
*/
private static Map<String, Object> loadConfiguredCacheValues(CacheConfiguration__mdt cacheConfiguration) {
Map<String, Object> keyToCacheValue = new Map<String, Object>();
if (cacheConfiguration.IsEnabled__c == false) {
Expand All @@ -139,6 +270,10 @@ global without sharing class CacheManager {
return keyToCacheValue;
}

/**
* @description Manages interacting with platform cache. The provided transaction cache instance is used internally as the primary
* caching method, and is further augmented by using Platform Cache to provide caching that spans multiple transactions.
*/
@SuppressWarnings('PMD.CognitiveComplexity, PMD.CyclomaticComplexity')
private class PlatformCache implements Cacheable {
private final PlatformCachePartitionProxy cachePartitionProxy;
Expand Down Expand Up @@ -296,7 +431,7 @@ global without sharing class CacheManager {
private final Cache.Partition platformCachePartition;

@SuppressWarnings('PMD.EmptyCatchBlock')
protected PlatformCachePartitionProxy(PlatformCacheType cacheType, String partitionName) {
protected PlatformCachePartitionProxy(PlatformCachePartitionType cacheType, String partitionName) {
// If the specified partition name is not found, the platform automatically throws a runtime exception, which isn't ideal.
// It seems better to eat the exceptions & fallback to the transaction cache (which doesn't rely on Platform Cache).
try {
Expand All @@ -309,8 +444,10 @@ global without sharing class CacheManager {
}
}
} catch (Cache.Org.OrgCacheException orgCacheException) {
System.Debug(LoggingLevel.WARN, '@#@ ⚠️ Organization Cache partition named ' + partitionName + ' not found.');
// No-op if the partition can't be found - the rest of the code will fallback to using the transaction cache
} catch (Cache.Session.SessionCacheException sessionCacheException) {
System.Debug(LoggingLevel.WARN, '@#@ ⚠️ Session Cache partition named ' + partitionName + ' not found.');
// No-op if the partition can't be found - the rest of the code will fallback to using the transaction cache
}
}
Expand Down Expand Up @@ -460,4 +597,4 @@ global without sharing class CacheManager {
}
}
}
}
}
4 changes: 2 additions & 2 deletions nebula-cache-manager/core/classes/CacheManager.cls-meta.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>56.0</apiVersion>
<apiVersion>61.0</apiVersion>
<status>Active</status>
</ApexClass>
Loading