Skip to content

Commit 8288b93

Browse files
authored
Merge pull request #8 from salesforcecli/t/tm-connect/removing-start-end-dates
W-22034567: remove startDate, endDate from license provision command and align json definition file fields with cli flags and enforce required flags
2 parents eb70540 + 6441275 commit 8288b93

5 files changed

Lines changed: 190 additions & 136 deletions

File tree

README.md

Lines changed: 25 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -115,16 +115,15 @@ Provision Permission Set Licenses (PSL) into a target org.
115115

116116
```
117117
USAGE
118-
$ sf license provision -o <value> [-n <value>] [-l <value>] [-q <value>] [-s <value>] [-e <value>] [-f <value>] [--api-version <value>] [--json] [--flags-dir <value>]
118+
$ sf license provision -o <value> [-l <value> -n <value> -q <value>] [-f <value>] [--api-version <value>] [--json] [--flags-dir <value>]
119119
120120
FLAGS
121-
-e, --end-date=<value> License end date in YYYY-MM-DD format. Default is no expiration.
122121
-f, --definition-file=<value> Path to a JSON file that contains the PSL provisioning request information.
123-
-l, --license=<value> Permission Set License name.
124-
-n, --namespace=<value> License package namespace.
122+
Cannot be combined with --license, --namespace, or --quantity.
123+
-l, --license=<value> Permission Set License name. Cannot be combined with --definition-file.
124+
-n, --namespace=<value> License package namespace. Requires --license. Cannot be combined with --definition-file.
125125
-o, --target-org=<value> (required) Username or alias of the target org.
126-
-q, --quantity=<value> Number of licenses to provision.
127-
-s, --start-date=<value> License start date in YYYY-MM-DD format. Defaults to today.
126+
-q, --quantity=<value> Number of licenses to provision. Requires --license. Cannot be combined with --definition-file.
128127
--api-version=<value> Override the api version used for api requests made by this command.
129128
130129
GLOBAL FLAGS
@@ -136,41 +135,33 @@ DESCRIPTION
136135
137136
There are two ways to run this command. You can provide the information to identify a single PSL via command line flags, or provision multiple PSLs in a single call by supplying a JSON formatted file.
138137
139-
See <Add URL Here> for the format and options contained within the JSON file.
138+
The JSON definition file must contain a top-level `licenses` array. Each entry supports the following fields:
140139
141-
EXAMPLES
142-
Provision a single Permission Set License into an org:
143-
144-
$ sf license provision --target-org myScratchOrg --namespace demo --license newLicense --quantity 5 --start-date '2026-03-30' --end-date '2027-03-30'
140+
| Field | Type | Required | Description |
141+
|---|---|---|---|
142+
| `license` | string | Yes | Permission Set License name. |
143+
| `namespace` | string | Yes | License package namespace. |
144+
| `quantity` | integer | Yes | Number of licenses to provision. |
145145
146-
Use a JSON formatted input file to provision one or more Permission Set Licenses into an org:
147-
148-
$ sf license provision --target-org myScratchOrg --definition-file test/config/provisionPSLs.json
146+
Example:
149147
150-
HUMAN READABLE OUTPUT
151-
152-
Success:
153-
Provisioned 5 licenses for the license definition 'demo__newLicense'
148+
json
149+
{
150+
"licenses": [
151+
{ "namespace": "myNS", "license": "premiumLicense", "quantity": 10 },
152+
{ "namespace": "myNS", "license": "starterLicense", "quantity": 5 }
153+
]
154+
}
154155
155-
Success:
156-
Provisioned 5 licenses for the license definition 'demo__newLicense'
157-
Provisioned 8 licenses for the license definition 'demo__premiumLicense'
156+
EXAMPLES
157+
Provision a single Permission Set License into an org:
158158
159-
Error: Failed to provision licenses.
160-
License Definition not found for 'demo__badLicense'.
161-
Quantity cannot be negative for 'demo__negativeLicense'.
159+
$ sf license provision --target-org myScratchOrg --namespace demo --license newLicense --quantity 5
162160
163-
JSON OUTPUT
161+
Use a JSON formatted input file to provision one or more Permission Set Licenses into an org:
164162
165-
{ "status": "success" }
163+
$ sf license provision --target-org myScratchOrg --definition-file test/config/provisionPSLs.json
166164
167-
{
168-
"status": "error",
169-
"messages": [
170-
{ "errorCode": "INVALID_LICENSE_DEFINITION", "message": "License definition not found for 'demo__badLicense'" },
171-
{ "errorCode": "INVALID_QUANTITY", "message": "Quantity cannot be negative for 'demo__negativeLicense'" }
172-
]
173-
}
174165
```
175166

176167
_See code: [src/commands/license/provision.ts](https://github.com/salesforcecli/plugin-license-management/blob/1.0.0/src/commands/license/provision.ts)_
@@ -186,5 +177,5 @@ sf package install --package <package-id> --target-org <scratch-org-username>
186177

187178
sf package install report -i <install-request-id> -o <scratch-org-username>
188179

189-
sf license provision -o <scratch-org-username> --license premium --namespace demo --quantity 10 --start-date '2026-03-20' --end-date '2027-03-20'
180+
sf license provision -o <scratch-org-username> --license premium --namespace demo --quantity 10
190181
```

command-snapshot.json

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,8 @@
33
"alias": [],
44
"command": "license:provision",
55
"flagAliases": ["apiversion", "targetusername", "u"],
6-
"flagChars": ["e", "f", "l", "n", "o", "q", "s"],
7-
"flags": [
8-
"api-version",
9-
"definition-file",
10-
"end-date",
11-
"flags-dir",
12-
"json",
13-
"license",
14-
"namespace",
15-
"quantity",
16-
"start-date",
17-
"target-org"
18-
],
6+
"flagChars": ["f", "l", "n", "o", "q"],
7+
"flags": ["api-version", "definition-file", "flags-dir", "json", "license", "namespace", "quantity", "target-org"],
198
"plugin": "@salesforce/plugin-license-management"
209
}
2110
]

messages/license.provision.md

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,6 @@ Permission Set License name.
2222

2323
Number of licenses to provision.
2424

25-
# flags.start-date.summary
26-
27-
License start date in YYYY-MM-DD format. Defaults to today.
28-
29-
# flags.end-date.summary
30-
31-
License end date in YYYY-MM-DD format. Default is no expiration.
32-
3325
# flags.definition-file.summary
3426

3527
Path to a JSON file that contains the PSL provisioning request information.
@@ -38,7 +30,7 @@ Path to a JSON file that contains the PSL provisioning request information.
3830

3931
- Provision a single Permission Set License into an org:
4032

41-
<%= config.bin %> <%= command.id %> --target-org myScratchOrg --namespace demo --license newLicense --quantity 5 --start-date '2026-03-30' --end-date '2027-03-30'
33+
<%= config.bin %> <%= command.id %> --target-org myScratchOrg --namespace demo --license newLicense --quantity 5
4234

4335
- Use a JSON formatted input file to provision one or more Permission Set Licenses into an org:
4436

@@ -52,14 +44,18 @@ Trace ID: %s
5244

5345
Either --license or --definition-file is required.
5446

55-
# error.mutuallyExclusiveFlags
56-
57-
The --definition-file flag cannot be used with --namespace, --license, --quantity, --start-date, or --end-date.
58-
5947
# error.emptyDefinitionFile
6048

6149
The definition file must contain at least one license entry.
6250

51+
# error.unsupportedDefinitionFileFields
52+
53+
Nonexistent fields: %s
54+
55+
# error.missingRequiredDefinitionFileFields
56+
57+
Missing required fields: %s
58+
6359
# error.provisionFailed
6460

6561
Failed to provision licenses. %s

src/commands/license/provision.ts

Lines changed: 52 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,25 @@ Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
2222
const messages = Messages.loadMessages('@salesforce/plugin-license-management', 'license.provision');
2323

2424
type ProvisionLicenseSpec = {
25+
namespace?: string;
26+
license?: string;
27+
quantity?: number;
28+
};
29+
30+
type ApiLicenseSpec = {
2531
namespacePrefix?: string;
2632
permissionSetLicense?: string;
2733
quantity?: number;
28-
startDate?: string;
29-
endDate?: string;
3034
};
3135

32-
type ProvisionPslRequest = {
36+
type DefinitionFile = {
3337
licenses: ProvisionLicenseSpec[];
3438
};
3539

40+
type ProvisionPslRequest = {
41+
licenses: ApiLicenseSpec[];
42+
};
43+
3644
type ProvisionPslResponse = {
3745
status: string;
3846
licensesProvisioned?: number;
@@ -46,8 +54,16 @@ export type LicenseProvisionResult = {
4654
};
4755

4856
function getLicenseDefinitionName(spec: ProvisionLicenseSpec): string {
49-
const psl = spec.permissionSetLicense ?? '';
50-
return spec.namespacePrefix ? `${spec.namespacePrefix}__${psl}` : psl;
57+
const psl = spec.license ?? '';
58+
return spec.namespace ? `${spec.namespace}__${psl}` : psl;
59+
}
60+
61+
function toApiSpec(spec: ProvisionLicenseSpec): ApiLicenseSpec {
62+
return {
63+
namespacePrefix: spec.namespace,
64+
permissionSetLicense: spec.license,
65+
quantity: spec.quantity,
66+
};
5167
}
5268

5369
export default class LicenseProvision extends SfCommand<LicenseProvisionResult> {
@@ -61,54 +77,57 @@ export default class LicenseProvision extends SfCommand<LicenseProvisionResult>
6177
namespace: Flags.string({
6278
char: 'n',
6379
summary: messages.getMessage('flags.namespace.summary'),
80+
dependsOn: ['license'],
81+
exclusive: ['definition-file'],
6482
}),
6583
license: Flags.string({
6684
char: 'l',
6785
summary: messages.getMessage('flags.license.summary'),
86+
exclusive: ['definition-file'],
87+
relationships: [{ type: 'all', flags: ['namespace', 'quantity'] }],
6888
}),
6989
quantity: Flags.integer({
7090
char: 'q',
7191
summary: messages.getMessage('flags.quantity.summary'),
7292
min: 0,
7393
max: Number.MAX_SAFE_INTEGER,
74-
}),
75-
'start-date': Flags.string({
76-
char: 's',
77-
summary: messages.getMessage('flags.start-date.summary'),
78-
}),
79-
'end-date': Flags.string({
80-
char: 'e',
81-
summary: messages.getMessage('flags.end-date.summary'),
94+
dependsOn: ['license'],
95+
exclusive: ['definition-file'],
8296
}),
8397
'definition-file': Flags.string({
8498
char: 'f',
8599
summary: messages.getMessage('flags.definition-file.summary'),
100+
exclusive: ['license', 'namespace', 'quantity'],
86101
}),
87102
};
88103

89104
// Protected to allow stubbing in tests
90-
protected static async loadSpecsFromFile(
91-
filePath: string,
92-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
93-
flags: Record<string, any>
94-
): Promise<ProvisionLicenseSpec[]> {
95-
if (
96-
flags['license'] ||
97-
flags['namespace'] ||
98-
flags['quantity'] !== undefined ||
99-
flags['start-date'] ||
100-
flags['end-date']
101-
) {
102-
throw messages.createError('error.mutuallyExclusiveFlags');
103-
}
104-
105+
protected static async loadSpecsFromFile(filePath: string): Promise<ProvisionLicenseSpec[]> {
105106
const fileContent = await readFile(filePath, 'utf-8');
106-
const definition = JSON.parse(fileContent) as ProvisionPslRequest;
107+
const definition = JSON.parse(fileContent) as DefinitionFile;
107108

108109
if (!Array.isArray(definition.licenses) || definition.licenses.length === 0) {
109110
throw messages.createError('error.emptyDefinitionFile');
110111
}
111112

113+
const allowedFields: ReadonlySet<string> = new Set(['namespace', 'license', 'quantity']);
114+
const unknownFields = [
115+
...new Set(definition.licenses.flatMap((entry) => Object.keys(entry).filter((key) => !allowedFields.has(key)))),
116+
];
117+
if (unknownFields.length > 0) {
118+
throw messages.createError('error.unsupportedDefinitionFileFields', [unknownFields.join(', ')]);
119+
}
120+
121+
const requiredFields = ['namespace', 'license', 'quantity'] as const;
122+
const missingFields = definition.licenses.flatMap((entry, index) =>
123+
requiredFields
124+
.filter((field) => entry[field] === undefined || entry[field] === null)
125+
.map((field) => `licenses[${index}].${field}`)
126+
);
127+
if (missingFields.length > 0) {
128+
throw messages.createError('error.missingRequiredDefinitionFileFields', [missingFields.join(', ')]);
129+
}
130+
112131
return definition.licenses;
113132
}
114133

@@ -118,15 +137,11 @@ export default class LicenseProvision extends SfCommand<LicenseProvisionResult>
118137
throw messages.createError('error.missingLicenseFlag');
119138
}
120139

121-
const startDate = (flags['start-date'] as string | undefined) ?? new Date().toISOString().slice(0, 10);
122-
123140
return [
124141
{
125-
namespacePrefix: flags['namespace'] as string | undefined,
126-
permissionSetLicense: flags['license'] as string,
142+
namespace: flags['namespace'] as string | undefined,
143+
license: flags['license'] as string,
127144
quantity: flags['quantity'] as number | undefined,
128-
startDate,
129-
endDate: flags['end-date'] as string | undefined,
130145
},
131146
];
132147
}
@@ -137,11 +152,11 @@ export default class LicenseProvision extends SfCommand<LicenseProvisionResult>
137152
const connection = flags['target-org'].getConnection(flags['api-version']);
138153

139154
const licenseSpecs = flags['definition-file']
140-
? await LicenseProvision.loadSpecsFromFile(flags['definition-file'], flags)
155+
? await LicenseProvision.loadSpecsFromFile(flags['definition-file'])
141156
: LicenseProvision.buildSpecsFromFlags(flags);
142157

143158
const endpoint = `/services/data/v${connection.getApiVersion()}/partnerdevelopment/permissionsetlicenses`;
144-
const requestBody: ProvisionPslRequest = { licenses: licenseSpecs };
159+
const requestBody: ProvisionPslRequest = { licenses: licenseSpecs.map(toApiSpec) };
145160

146161
let response: ProvisionPslResponse;
147162
try {

0 commit comments

Comments
 (0)