Skip to content

Commit 6441275

Browse files
committed
feat: align json definition file fields with cli flags and enforce required flags
1 parent a57f9a9 commit 6441275

4 files changed

Lines changed: 183 additions & 68 deletions

File tree

README.md

Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -115,14 +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>] [-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
121121
-f, --definition-file=<value> Path to a JSON file that contains the PSL provisioning request information.
122-
-l, --license=<value> Permission Set License name.
123-
-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.
124125
-o, --target-org=<value> (required) Username or alias of the target org.
125-
-q, --quantity=<value> Number of licenses to provision.
126+
-q, --quantity=<value> Number of licenses to provision. Requires --license. Cannot be combined with --definition-file.
126127
--api-version=<value> Override the api version used for api requests made by this command.
127128
128129
GLOBAL FLAGS
@@ -134,41 +135,33 @@ DESCRIPTION
134135
135136
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.
136137
137-
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:
138139
139-
EXAMPLES
140-
Provision a single Permission Set License into an org:
141-
142-
$ sf license provision --target-org myScratchOrg --namespace demo --license newLicense --quantity 5
143-
144-
Use a JSON formatted input file to provision one or more Permission Set Licenses into an org:
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-
$ sf license provision --target-org myScratchOrg --definition-file test/config/provisionPSLs.json
147-
148-
HUMAN READABLE OUTPUT
146+
Example:
149147
150-
Success:
151-
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+
}
152155
153-
Success:
154-
Provisioned 5 licenses for the license definition 'demo__newLicense'
155-
Provisioned 8 licenses for the license definition 'demo__premiumLicense'
156+
EXAMPLES
157+
Provision a single Permission Set License into an org:
156158
157-
Error: Failed to provision licenses.
158-
License Definition not found for 'demo__badLicense'.
159-
Quantity cannot be negative for 'demo__negativeLicense'.
159+
$ sf license provision --target-org myScratchOrg --namespace demo --license newLicense --quantity 5
160160
161-
JSON OUTPUT
161+
Use a JSON formatted input file to provision one or more Permission Set Licenses into an org:
162162
163-
{ "status": "success" }
163+
$ sf license provision --target-org myScratchOrg --definition-file test/config/provisionPSLs.json
164164
165-
{
166-
"status": "error",
167-
"messages": [
168-
{ "errorCode": "INVALID_LICENSE_DEFINITION", "message": "License definition not found for 'demo__badLicense'" },
169-
{ "errorCode": "INVALID_QUANTITY", "message": "Quantity cannot be negative for 'demo__negativeLicense'" }
170-
]
171-
}
172165
```
173166

174167
_See code: [src/commands/license/provision.ts](https://github.com/salesforcecli/plugin-license-management/blob/1.0.0/src/commands/license/provision.ts)_

messages/license.provision.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,18 @@ Trace ID: %s
4444

4545
Either --license or --definition-file is required.
4646

47-
# error.mutuallyExclusiveFlags
48-
49-
The --definition-file flag cannot be used with --namespace, --license, or --quantity.
50-
5147
# error.emptyDefinitionFile
5248

5349
The definition file must contain at least one license entry.
5450

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

5761
Failed to provision licenses. %s

src/commands/license/provision.ts

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +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;
2834
};
2935

30-
type ProvisionPslRequest = {
36+
type DefinitionFile = {
3137
licenses: ProvisionLicenseSpec[];
3238
};
3339

40+
type ProvisionPslRequest = {
41+
licenses: ApiLicenseSpec[];
42+
};
43+
3444
type ProvisionPslResponse = {
3545
status: string;
3646
licensesProvisioned?: number;
@@ -44,8 +54,16 @@ export type LicenseProvisionResult = {
4454
};
4555

4656
function getLicenseDefinitionName(spec: ProvisionLicenseSpec): string {
47-
const psl = spec.permissionSetLicense ?? '';
48-
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+
};
4967
}
5068

5169
export default class LicenseProvision extends SfCommand<LicenseProvisionResult> {
@@ -59,40 +77,57 @@ export default class LicenseProvision extends SfCommand<LicenseProvisionResult>
5977
namespace: Flags.string({
6078
char: 'n',
6179
summary: messages.getMessage('flags.namespace.summary'),
80+
dependsOn: ['license'],
81+
exclusive: ['definition-file'],
6282
}),
6383
license: Flags.string({
6484
char: 'l',
6585
summary: messages.getMessage('flags.license.summary'),
86+
exclusive: ['definition-file'],
87+
relationships: [{ type: 'all', flags: ['namespace', 'quantity'] }],
6688
}),
6789
quantity: Flags.integer({
6890
char: 'q',
6991
summary: messages.getMessage('flags.quantity.summary'),
7092
min: 0,
7193
max: Number.MAX_SAFE_INTEGER,
94+
dependsOn: ['license'],
95+
exclusive: ['definition-file'],
7296
}),
7397
'definition-file': Flags.string({
7498
char: 'f',
7599
summary: messages.getMessage('flags.definition-file.summary'),
100+
exclusive: ['license', 'namespace', 'quantity'],
76101
}),
77102
};
78103

79104
// Protected to allow stubbing in tests
80-
protected static async loadSpecsFromFile(
81-
filePath: string,
82-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
83-
flags: Record<string, any>
84-
): Promise<ProvisionLicenseSpec[]> {
85-
if (flags['license'] || flags['namespace'] || flags['quantity'] !== undefined) {
86-
throw messages.createError('error.mutuallyExclusiveFlags');
87-
}
88-
105+
protected static async loadSpecsFromFile(filePath: string): Promise<ProvisionLicenseSpec[]> {
89106
const fileContent = await readFile(filePath, 'utf-8');
90-
const definition = JSON.parse(fileContent) as ProvisionPslRequest;
107+
const definition = JSON.parse(fileContent) as DefinitionFile;
91108

92109
if (!Array.isArray(definition.licenses) || definition.licenses.length === 0) {
93110
throw messages.createError('error.emptyDefinitionFile');
94111
}
95112

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+
96131
return definition.licenses;
97132
}
98133

@@ -104,8 +139,8 @@ export default class LicenseProvision extends SfCommand<LicenseProvisionResult>
104139

105140
return [
106141
{
107-
namespacePrefix: flags['namespace'] as string | undefined,
108-
permissionSetLicense: flags['license'] as string,
142+
namespace: flags['namespace'] as string | undefined,
143+
license: flags['license'] as string,
109144
quantity: flags['quantity'] as number | undefined,
110145
},
111146
];
@@ -117,11 +152,11 @@ export default class LicenseProvision extends SfCommand<LicenseProvisionResult>
117152
const connection = flags['target-org'].getConnection(flags['api-version']);
118153

119154
const licenseSpecs = flags['definition-file']
120-
? await LicenseProvision.loadSpecsFromFile(flags['definition-file'], flags)
155+
? await LicenseProvision.loadSpecsFromFile(flags['definition-file'])
121156
: LicenseProvision.buildSpecsFromFlags(flags);
122157

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

126161
let response: ProvisionPslResponse;
127162
try {

0 commit comments

Comments
 (0)