├── .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 | [](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t4x000000NYNEAA4)
8 | [](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 |
--------------------------------------------------------------------------------