Skip to content

Commit 262e8a4

Browse files
committed
feat: accept 15-char package version IDs in bundle definition file
1 parent bc50535 commit 262e8a4

4 files changed

Lines changed: 129 additions & 1 deletion

File tree

src/package/packageBundleVersionCreate.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import * as fs from 'node:fs';
1717
import { Connection, Messages, SfError, SfProject } from '@salesforce/core';
1818
import { BundleSObjects, BundleVersionCreateOptions } from '../interfaces';
1919
import { massageErrorMessage } from '../utils/bundleUtils';
20+
import { convertTo18CharId } from '../utils/packageUtils';
2021

2122
Messages.importMessagesDirectory(__dirname);
2223
const messages = Messages.loadMessages('@salesforce/packaging', 'bundle_version_create');
@@ -174,11 +175,16 @@ export class PackageBundleVersionCreate {
174175
return bundleVersionComponents.map((component) => {
175176
const packageVersion = component.packageVersion;
176177

177-
// Check if it's already an ID (04t followed by 15 characters)
178+
// Check if it's already an 18-char ID (04t followed by 15 characters)
178179
if (/^04t[a-zA-Z0-9]{15}$/.test(packageVersion)) {
179180
return packageVersion;
180181
}
181182

183+
// Check if it's a 15-char ID (04t followed by 12 characters) and convert to 18-char
184+
if (/^04t[a-zA-Z0-9]{12}$/.test(packageVersion)) {
185+
return convertTo18CharId(packageVersion);
186+
}
187+
182188
// Otherwise, treat it as an alias and resolve it from sfdx-project.json
183189
const packageVersionId = project.getPackageIdFromAlias(packageVersion);
184190
if (!packageVersionId) {

src/utils/packageUtils.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,33 @@ export function uniqid(options?: { template?: string; length?: number }): string
8888
: `${options.template}${uniqueString}`;
8989
}
9090

91+
/**
92+
* Converts a 15-character Salesforce ID to its 18-character case-insensitive equivalent.
93+
* Returns the ID unchanged if it is not exactly 15 characters.
94+
*
95+
* @param id - The Salesforce ID to convert
96+
* @returns The 18-character Salesforce ID
97+
*/
98+
export function convertTo18CharId(id: string): string {
99+
if (!id || id.length !== 15) {
100+
return id;
101+
}
102+
103+
const suffix: string[] = [];
104+
for (let i = 0; i < 3; i++) {
105+
let flags = 0;
106+
for (let j = 0; j < 5; j++) {
107+
const char = id.charAt(i * 5 + j);
108+
if (char >= 'A' && char <= 'Z') {
109+
flags += 1 << j;
110+
}
111+
}
112+
suffix.push('ABCDEFGHIJKLMNOPQRSTUVWXYZ012345'.charAt(flags));
113+
}
114+
115+
return id + suffix.join('');
116+
}
117+
91118
export function validateId(idObj: Many<IdRegistryValue>, value: string | undefined): void {
92119
if (!value || !validateIdNoThrow(idObj, value)) {
93120
throw messages.createError('invalidIdOrAlias', [

test/package/bundleVersionCreate.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,75 @@ describe('PackageBundleVersion.create', () => {
585585
fs.unlinkSync(componentsPath);
586586
});
587587

588+
it('should accept 15-char package version IDs and convert to 18-char', async () => {
589+
const componentsPath = path.join(project.getPath(), 'bundle-components.json');
590+
const components = [
591+
{ packageVersion: '04t5f000000WM9y' }, // 15-char ID
592+
{ packageVersion: '04t000000000000003' }, // 18-char ID (should stay unchanged)
593+
];
594+
fs.writeFileSync(componentsPath, JSON.stringify(components));
595+
596+
let capturedRequest: { BundleVersionComponents: string } | undefined;
597+
Object.assign(connection.tooling, {
598+
sobject: () => ({
599+
create: (req: { BundleVersionComponents: string }) => {
600+
capturedRequest = req;
601+
return Promise.resolve({
602+
success: true,
603+
id: '0Ho000000000000',
604+
});
605+
},
606+
}),
607+
query: () =>
608+
Promise.resolve({
609+
records: [{ BundleName: 'testBundle' }],
610+
}),
611+
});
612+
613+
Object.assign(connection, {
614+
autoFetchQuery: () =>
615+
Promise.resolve({
616+
records: [
617+
{
618+
Id: '0Ho000000000000',
619+
RequestStatus: BundleSObjects.PkgBundleVersionCreateReqStatus.success,
620+
PackageBundle: { Id: '0Ho123456789012' },
621+
PackageBundleVersion: { Id: '1Q8000000000001' },
622+
VersionName: 'ver 1.0',
623+
MajorVersion: '1',
624+
MinorVersion: '0',
625+
Ancestor: null,
626+
BundleVersionComponents: JSON.stringify(components),
627+
CreatedDate: '2025-01-01T00:00:00.000Z',
628+
CreatedById: '005000000000000',
629+
ValidationError: '',
630+
},
631+
],
632+
}),
633+
});
634+
635+
const options: BundleVersionCreateOptions = {
636+
connection,
637+
project,
638+
PackageBundle: 'testBundle',
639+
MajorVersion: '1',
640+
MinorVersion: '0',
641+
Ancestor: null,
642+
BundleVersionComponentsPath: componentsPath,
643+
};
644+
645+
const result = await PackageBundleVersion.create(options);
646+
expect(result).to.have.property('RequestStatus', BundleSObjects.PkgBundleVersionCreateReqStatus.success);
647+
648+
// Verify the 15-char ID was converted to 18-char in the API request
649+
expect(capturedRequest).to.not.be.undefined;
650+
const sentComponents = JSON.parse(capturedRequest!.BundleVersionComponents) as string[];
651+
expect(sentComponents[0]).to.equal('04t5f000000WM9yAAG'); // converted from 15 to 18
652+
expect(sentComponents[1]).to.equal('04t000000000000003'); // unchanged 18-char
653+
654+
fs.unlinkSync(componentsPath);
655+
});
656+
588657
it('should handle invalid bundle components format', async () => {
589658
const componentsPath = path.join(project.getPath(), 'bundle-components.json');
590659

test/utils/packageUtils.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import JSZIP from 'jszip';
2626
import {
2727
applyErrorAction,
2828
combineSaveErrors,
29+
convertTo18CharId,
2930
findPackageDirectory,
3031
resolveBuildUserPermissions,
3132
getPackageVersionNumber,
@@ -49,6 +50,31 @@ describe('packageUtils', () => {
4950
restoreContext($$);
5051
});
5152

53+
describe('convertTo18CharId', () => {
54+
it('should convert a 15-char ID to 18-char', () => {
55+
expect(convertTo18CharId('04t5f000000WM9y')).to.equal('04t5f000000WM9yAAG');
56+
});
57+
58+
it('should return an 18-char ID unchanged', () => {
59+
expect(convertTo18CharId('04t5f000000WM9yAAG')).to.equal('04t5f000000WM9yAAG');
60+
});
61+
62+
it('should return empty string unchanged', () => {
63+
expect(convertTo18CharId('')).to.equal('');
64+
});
65+
66+
it('should handle all-lowercase 15-char ID', () => {
67+
const result = convertTo18CharId('04t5f000000wm9y');
68+
expect(result).to.have.lengthOf(18);
69+
expect(result).to.equal('04t5f000000wm9yAAA');
70+
});
71+
72+
it('should handle mixed-case 15-char ID', () => {
73+
const result = convertTo18CharId('001A000001ABCDE');
74+
expect(result).to.have.lengthOf(18);
75+
});
76+
});
77+
5278
describe('getPackage2VersionNumber', () => {
5379
it('should return the correct version number', () => {
5480
const version = {

0 commit comments

Comments
 (0)