├── .husky └── pre-commit ├── images ├── btn-install-unlocked-package-sandbox.png └── btn-install-unlocked-package-production.png ├── apex-uuid └── classes │ ├── Uuid.cls-meta.xml │ ├── Uuid_Tests.cls-meta.xml │ ├── Uuid_Tests.cls │ └── Uuid.cls ├── .prettierignore ├── .prettierrc ├── .eslintignore ├── config └── project-scratch-def.json ├── .forceignore ├── .github └── workflows │ └── fetch-repo-stats.yml ├── sfdx-project.json ├── .gitignore ├── LICENSE ├── package.json └── README.md /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run precommit -------------------------------------------------------------------------------- /images/btn-install-unlocked-package-sandbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jongpie/ApexUUID/HEAD/images/btn-install-unlocked-package-sandbox.png -------------------------------------------------------------------------------- /images/btn-install-unlocked-package-production.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jongpie/ApexUUID/HEAD/images/btn-install-unlocked-package-production.png -------------------------------------------------------------------------------- /apex-uuid/classes/Uuid.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 56.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /apex-uuid/classes/Uuid_Tests.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 56.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # List files or directories below to ignore them when running prettier 2 | # More information: https://prettier.io/docs/en/ignore.html 3 | # 4 | 5 | **/staticresources/** 6 | .localdevserver 7 | .sfdx 8 | .vscode 9 | 10 | coverage/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "overrides": [ 4 | { 5 | "files": "**/lwc/**/*.html", 6 | "options": { "parser": "lwc" } 7 | }, 8 | { 9 | "files": "*.{cmp,page,component}", 10 | "options": { "parser": "html" } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/lwc/**/*.css 2 | **/lwc/**/*.html 3 | **/lwc/**/*.json 4 | **/lwc/**/*.svg 5 | **/lwc/**/*.xml 6 | **/aura/**/*.auradoc 7 | **/aura/**/*.cmp 8 | **/aura/**/*.css 9 | **/aura/**/*.design 10 | **/aura/**/*.evt 11 | **/aura/**/*.json 12 | **/aura/**/*.svg 13 | **/aura/**/*.tokens 14 | **/aura/**/*.xml 15 | **/aura/**/*.app 16 | .sfdx 17 | -------------------------------------------------------------------------------- /config/project-scratch-def.json: -------------------------------------------------------------------------------- 1 | { 2 | "orgName": "Demo company", 3 | "edition": "Developer", 4 | "features": ["EnableSetPasswordInApi"], 5 | "settings": { 6 | "lightningExperienceSettings": { 7 | "enableS1DesktopEnabled": true 8 | }, 9 | "mobileSettings": { 10 | "enableS1EncryptedStoragePref2": false 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.forceignore: -------------------------------------------------------------------------------- 1 | # List files or directories below to ignore them when running force:source:push, force:source:pull, and force:source:status 2 | # More information: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm 3 | # 4 | 5 | package.xml 6 | 7 | # LWC configuration files 8 | **/jsconfig.json 9 | **/.eslintrc.json 10 | 11 | # LWC Jest 12 | **/__tests__/** -------------------------------------------------------------------------------- /.github/workflows/fetch-repo-stats.yml: -------------------------------------------------------------------------------- 1 | name: Fetch Repo Stats 2 | 3 | on: 4 | schedule: 5 | # Run this once per day, towards the end of the day for keeping the most 6 | # recent data point most meaningful (hours are interpreted in UTC). 7 | - cron: "0 23 * * *" 8 | workflow_dispatch: # Allow for running this manually. 9 | 10 | jobs: 11 | j1: 12 | name: store-repo-stats 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: run-ghrs 16 | uses: jgehrcke/github-repo-stats@RELEASE 17 | with: 18 | ghtoken: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} 19 | -------------------------------------------------------------------------------- /sfdx-project.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageDirectories": [ 3 | { 4 | "package": "Apex UUID", 5 | "path": "apex-uuid", 6 | "definitionFile": "./config/project-scratch-def.json", 7 | "versionNumber": "2.1.0.0", 8 | "versionName": "Unlocked Package Release", 9 | "versionDescription": "Initial release of new unlocked package (no namespace)", 10 | "releaseNotesUrl": "https://github.com/jongpie/ApexUuid/releases", 11 | "default": true 12 | } 13 | ], 14 | "name": "ApexUUID", 15 | "namespace": "", 16 | "sfdcLoginUrl": "https://login.salesforce.com", 17 | "sourceApiVersion": "55.0", 18 | "packageAliases": { 19 | "Apex UUID": "0Ho4x0000008OYtCAM", 20 | "Apex UUID@2.1.0-0": "04t4x000000NYNEAA4" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is used for Git repositories to specify intentionally untracked files that Git should ignore. 2 | # If you are not using git, you can delete this file. For more information see: https://git-scm.com/docs/gitignore 3 | # For useful gitignore templates see: https://github.com/github/gitignore 4 | 5 | # Salesforce cache 6 | .sf/ 7 | .sfdx/ 8 | .localdevserver/ 9 | .vscode/ 10 | deploy-options.json 11 | 12 | # LWC VSCode autocomplete 13 | **/lwc/jsconfig.json 14 | 15 | # Code coverage reports 16 | test-coverage/ 17 | 18 | # Logs 19 | logs 20 | *.log 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # Dependency directories 26 | node_modules/ 27 | 28 | # Eslint cache 29 | .eslintcache 30 | 31 | # MacOS system files 32 | .DS_Store 33 | 34 | # Windows system files 35 | Thumbs.db 36 | ehthumbs.db 37 | [Dd]esktop.ini 38 | $RECYCLE.BIN/ 39 | 40 | # Local environment variables 41 | .env -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jonathan Gillespie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apex-uuid", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "Apex UUID", 6 | "scripts": { 7 | "lint": "eslint **/{aura,lwc}/**", 8 | "package:version:create": "echo hello && sfdx force:package:version:create --json --package \"Apex UUID\" --skipancestorcheck --codecoverage --installationkeybypass --wait 30 && prettier sfdx-project.json", 9 | "test:apex": "sfdx force:apex:test:run --verbose --testlevel RunLocalTests --wait 30 --resultformat human --codecoverage --detailedcoverage --outputdir ./test-coverage/apex", 10 | "prettier": "prettier --write \"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}\"", 11 | "prettier:verify": "prettier --list-different \"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}\"", 12 | "postinstall": "husky install", 13 | "precommit": "lint-staged" 14 | }, 15 | "devDependencies": { 16 | "@prettier/plugin-xml": "^2.0.1", 17 | "@salesforce/eslint-config-lwc": "^3.2.3", 18 | "@salesforce/eslint-plugin-aura": "^2.0.0", 19 | "@salesforce/eslint-plugin-lightning": "^1.0.0", 20 | "@salesforce/sfdx-lwc-jest": "^1.1.0", 21 | "husky": "^7.0.4", 22 | "lint-staged": "^12.3.7", 23 | "prettier": "^2.6.0", 24 | "prettier-plugin-apex": "^1.10.0" 25 | }, 26 | "lint-staged": { 27 | "**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}": [ 28 | "prettier --write" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /apex-uuid/classes/Uuid_Tests.cls: -------------------------------------------------------------------------------- 1 | /****************************************************************************************** 2 | * This file is part of the Apex UUID project, released under the MIT License. * 3 | * See LICENSE file or go to https://github.com/jongpie/ApexUuid for full license details. * 4 | ******************************************************************************************/ 5 | @isTest 6 | private class Uuid_Tests { 7 | @isTest 8 | static void it_should_create_several_valid_uuids() { 9 | String generatedUuid = new Uuid().getValue(); 10 | System.assertEquals(36, generatedUuid.length()); 11 | 12 | Pattern pattern = Pattern.compile( 13 | '[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}' 14 | ); 15 | 16 | for (Integer i = 0; i < 10; i++) { 17 | Uuid uuid = new Uuid(); 18 | Matcher matcher = pattern.matcher(uuid.getValue()); 19 | System.assert(matcher.matches(), 'Generated UUID=' + uuid.getValue()); 20 | } 21 | } 22 | 23 | @isTest 24 | static void it_should_reuse_a_uuid_on_subsequent_calls() { 25 | Uuid uuid = new Uuid(); 26 | String originalValue = uuid.getValue(); 27 | 28 | for (Integer i = 0; i < 5; i++) { 29 | System.assertEquals(originalValue, uuid.getValue()); 30 | } 31 | } 32 | 33 | @isTest 34 | static void it_should_verify_that_a_uuid_is_a_uuid() { 35 | String generatedUuid = new Uuid().getValue(); 36 | System.assert(Uuid.isValid(generatedUuid)); 37 | } 38 | 39 | @isTest 40 | static void it_should_not_consider_a_blank_string_a_uuid() { 41 | System.assertEquals(false, Uuid.isValid('')); 42 | } 43 | 44 | @isTest 45 | static void it_should_not_consider_null_a_uuid() { 46 | System.assertEquals(false, Uuid.isValid(null)); 47 | } 48 | 49 | @isTest 50 | static void it_should_validate_a_uuid_in_upper_case() { 51 | String exampleUuid = 'f3665813-1a60-4924-ad9b-23a9cef17d80'.toUpperCase(); 52 | System.assertEquals(true, Uuid.isValid(exampleUuid)); 53 | } 54 | 55 | @isTest 56 | static void it_should_validate_a_uuid_in_lower_case() { 57 | String exampleUuid = 'f3665813-1a60-4924-ad9b-23a9cef17d80'.toLowerCase(); 58 | System.assertEquals(true, Uuid.isValid(exampleUuid)); 59 | } 60 | 61 | @isTest 62 | static void it_should_convert_a_valid_string_to_a_uuid() { 63 | String uuidValue = new Uuid().getValue(); 64 | 65 | Test.startTest(); 66 | Uuid convertedUuid = Uuid.valueOf(uuidValue); 67 | Test.stopTest(); 68 | 69 | System.assertEquals(uuidValue, convertedUuid.getValue()); 70 | } 71 | 72 | @isTest 73 | static void it_should_not_convert_an_invalid_string_to_a_uuid() { 74 | String invalidUuidValue = 'this-is-not-a-valid-uuid'; 75 | 76 | Test.startTest(); 77 | try { 78 | Uuid convertedUuid = Uuid.valueOf(invalidUuidValue); 79 | System.assert(false, 'Error expected here'); 80 | } catch (Exception ex) { 81 | String expectedError = invalidUuidValue + ' is not a valid UUID'; 82 | System.assert(ex.getMessage().contains(expectedError)); 83 | } 84 | Test.stopTest(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apex UUID 2 | 3 | Provides a way to generate a [UUID (Universally Unique Identifier)](https://en.wikipedia.org/wiki/Universally_unique_identifier) in Salesforce's Apex language. This uses Verion 4 of the UUID standard - more details available [here]() 4 | 5 | ## Unlocked Package (no namespace) - v2.1.0 6 | 7 | [![Install Unlocked Package in a Sandbox](./images/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t4x000000NYNEAA4) 8 | [![Install Unlocked Package in Production](./images/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t4x000000NYNEAA4) 9 | 10 | # Getting Started 11 | 12 | ## Generating & Using A UUID 13 | 14 | To generate a UUID, simply instantiate a new instance. The string value can be retrieved with `getValue()` 15 | 16 | ```java 17 | Uuid myUuid = new Uuid(); 18 | String myUuidValue = myUuid.getValue(); 19 | ``` 20 | 21 | You can use the UUID value as a unique ID, generated by Apex. This lets you do some powerful things in Apex when your objects have external ID fields to store the UUID value. 22 | 23 | It's best if you create a custom field on your object to store the UUID value with these properties: Text(36) (External ID) (Unique Case Insensitive). 24 | 25 | In the code samples below, the field name Uuid\_\_c is used as an example field name. 26 | 27 | > #### Example: Using a code-generated external ID (such as a UUID), we can create multiple accounts and related contacts with only 1 DML statement 28 | 29 | ```java 30 | List recordsToCreate = new List(); 31 | // Create 10 sample accounts 32 | for(Integer accountCount = 0; accountCount < 10; accountCount++) { 33 | // Create a new account & set a custom external ID text field called Uuid__c 34 | Account newAccount = new Account( 35 | Name = 'Account ' + accountCount, 36 | Uuid__c = new Uuid().getValue() 37 | ); 38 | recordsToCreate.add(newAccount); 39 | 40 | // For each sample account, create 10 sample contacts 41 | for(Integer contactCount = 0; contactCount < 10; contactCount++) { 42 | // Instead of setting contact.AccountId with a Salesforce ID... 43 | // we can use an Account object with a Uuid__c value to set the Contact-Account relationship 44 | Contact newContact = new Contact( 45 | Account = new Account(Uuid__c = newAccount.Uuid__c), 46 | LastName = 'Contact ' + contactCount 47 | ); 48 | recordsToCreate.add(newContact); 49 | } 50 | } 51 | // Sort so that the accounts are created before contacts (accounts are the parent object) 52 | recordsToCreate.sort(); 53 | insert recordsToCreate; 54 | 55 | // Verify that we only used 1 DML statement 56 | System.assertEquals(1, Limits.getDmlStatements()); 57 | ``` 58 | 59 | ### Using a UUID's string value 60 | 61 | If you already have a UUID as a string (previously generated in Apex, generated by an external system, etc), there are 3 static methods to help work with the string value. 62 | 63 | 1. **Do I have a valid UUID value?** 64 | 65 | This checks if the string value matches the regex for a UUID v4, including hyphens (but it is case-insensitive) 66 | 67 | ```java 68 | Boolean isValid = Uuid.isValid(myUuidValue); 69 | ``` 70 | 71 | 2. **I have a UUID value but need to format it** 72 | 73 | This returns a formatted string that follows the UUID pattern 8-4-4-4-12 in lowercase. If an invalid string is provided, a UuidException is thrown. 74 | 75 | ```java 76 | String formattedUuidValue = Uuid.formatValue(myUnformattedUuidValue); 77 | ``` 78 | 79 | 3. **I have a UUID value, how can I use it to construct a UUID?** 80 | 81 | This will automatically format the value for you, but the intial value must be a valid (unformatted) UUID string 82 | 83 | ```java 84 | Uuid myUuid = Uuid.valueOf(myUuidValue); 85 | ``` 86 | -------------------------------------------------------------------------------- /apex-uuid/classes/Uuid.cls: -------------------------------------------------------------------------------- 1 | /****************************************************************************************** 2 | * This file is part of the Apex UUID project, released under the MIT License. * 3 | * See LICENSE file or go to https://github.com/jongpie/ApexUuid for full license details. * 4 | ******************************************************************************************/ 5 | public without sharing class Uuid { 6 | private static final Integer HEX_BASE = HEX_CHARACTERS.length(); 7 | private static final String HEX_CHARACTERS = '0123456789abcdef'; 8 | private static final String HEX_PREFIX = '0x'; 9 | private static final List HEX_CHARACTER_LIST = HEX_CHARACTERS.split( 10 | '' 11 | ); 12 | private static final Integer UUID_V4_LENGTH = 36; 13 | private static final String UUID_V4_REGEX = '[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}'; 14 | 15 | public static String formatValue(String unformattedValue) { 16 | final String invalidValueError = 17 | unformattedValue + ' is not a valid UUID value'; 18 | 19 | // Remove any non-alphanumeric characters 20 | unformattedValue = unformattedValue.replaceAll('[^a-zA-Z0-9]', ''); 21 | 22 | // If the unformatted value isn't even the right length to be valid, then throw an exception 23 | // Subtract 4 because the UUID_V4_LENGTH includes 4 '-' characters in the UUID pattern 24 | if (unformattedValue.length() != (UUID_V4_LENGTH - 4)) 25 | throw new UuidException(invalidValueError); 26 | 27 | // UUID Pattern: 8-4-4-4-12 28 | String formattedValue = 29 | unformattedValue.substring(0, 8) + 30 | '-' + 31 | unformattedValue.substring(8, 12) + 32 | '-' + 33 | unformattedValue.substring(12, 16) + 34 | '-' + 35 | unformattedValue.substring(16, 20) + 36 | '-' + 37 | unformattedValue.substring(20); 38 | 39 | formattedValue = formattedValue.toLowerCase(); 40 | 41 | if (!Uuid.isValid(formattedValue)) 42 | throw new UuidException(invalidValueError); 43 | 44 | return formattedValue; 45 | } 46 | 47 | public static Boolean isValid(String uuidValue) { 48 | if (String.isBlank(uuidValue)) 49 | return false; 50 | if (uuidValue.length() != UUID_V4_LENGTH) 51 | return false; 52 | 53 | Pattern uuidPattern = Pattern.compile(UUID_V4_REGEX.toLowerCase()); 54 | Matcher uuidMatcher = uuidPattern.matcher(uuidValue.toLowerCase()); 55 | 56 | return uuidMatcher.matches(); 57 | } 58 | 59 | public static Uuid valueOf(String uuidValue) { 60 | return new Uuid(uuidValue); 61 | } 62 | 63 | private final String value; 64 | 65 | public Uuid() { 66 | this.value = this.generateValue(); 67 | } 68 | 69 | private Uuid(String uuidValue) { 70 | this.value = Uuid.formatValue(uuidValue); 71 | } 72 | 73 | public String getValue() { 74 | return this.value; 75 | } 76 | 77 | private String generateValue() { 78 | String hexValue = EncodingUtil.convertToHex(Crypto.generateAesKey(128)); 79 | 80 | // Version Calculation: (i & 0x0f) | 0x40 81 | // Version Format: Always begins with 4 82 | String versionShiftedHexBits = this.getShiftedHexBits( 83 | hexValue.substring(14, 16), 84 | this.convertHexToInteger('0x0f'), 85 | this.convertHexToInteger('0x40') 86 | ); 87 | 88 | // Variant Calculation: (i & 0x3f) | 0x80 89 | // Variant Format: Always begins with 8, 9, A or B 90 | String variantShiftedHexBits = this.getShiftedHexBits( 91 | hexValue.substring(18, 20), 92 | this.convertHexToInteger('0x3f'), 93 | this.convertHexToInteger('0x80') 94 | ); 95 | 96 | String uuidValue = 97 | hexValue.substring(0, 8) + // time-low 98 | hexValue.substring(8, 12) + // time-mid 99 | versionShiftedHexBits + 100 | hexValue.substring(14, 16) + // time-high-and-version 101 | variantShiftedHexBits + 102 | hexValue.substring(18, 20) + // clock-seq-and-reserved + clock-seq-low 103 | hexValue.substring(20); // node 104 | 105 | return Uuid.formatValue(uuidValue); 106 | } 107 | 108 | private String getShiftedHexBits( 109 | String hexSubstring, 110 | Integer lowerThreshold, 111 | Integer upperThreshold 112 | ) { 113 | Integer shiftedIntegerBits = 114 | (this.convertHexToInteger(hexSubstring) & lowerThreshold) | 115 | upperThreshold; 116 | return this.convertIntegerToHex(shiftedIntegerBits); 117 | } 118 | 119 | private Integer convertHexToInteger(String hexValue) { 120 | hexValue = hexValue.toLowerCase(); 121 | 122 | if (hexValue.startsWith(HEX_PREFIX)) 123 | hexValue = hexValue.substringAfter(HEX_PREFIX); 124 | 125 | Integer integerValue = 0; 126 | for (String hexCharacter : hexValue.split('')) { 127 | Integer hexCharacterIndex = HEX_CHARACTERS.indexOf(hexCharacter); 128 | 129 | integerValue = HEX_BASE * integerValue + hexCharacterIndex; 130 | } 131 | return integerValue; 132 | } 133 | 134 | private String convertIntegerToHex(Integer integerValue) { 135 | String hexValue = ''; 136 | while (integerValue > 0) { 137 | Integer hexCharacterIndex = Math.mod(integerValue, HEX_BASE); 138 | 139 | hexValue = HEX_CHARACTER_LIST[hexCharacterIndex] + hexValue; 140 | integerValue = integerValue / HEX_BASE; 141 | } 142 | return hexValue; 143 | } 144 | 145 | private class UuidException extends Exception { 146 | } 147 | } 148 | --------------------------------------------------------------------------------