sf-plugin-permission-sets 0.0.0-dev.15 → 0.0.0-dev.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -6
- package/lib/adapters/org-client.d.ts +20 -0
- package/lib/adapters/org-client.js +35 -0
- package/lib/adapters/org-client.js.map +1 -0
- package/lib/commands/ps/check.js +4 -3
- package/lib/commands/ps/check.js.map +1 -1
- package/lib/commands/ps/validate.d.ts +19 -0
- package/lib/commands/ps/validate.js +48 -0
- package/lib/commands/ps/validate.js.map +1 -0
- package/lib/core/load.d.ts +1 -6
- package/lib/core/load.js +4 -4
- package/lib/core/load.js.map +1 -1
- package/lib/core/model.d.ts +5 -0
- package/lib/core/resolve.d.ts +14 -0
- package/lib/core/resolve.js +69 -0
- package/lib/core/resolve.js.map +1 -0
- package/lib/services/check.d.ts +6 -5
- package/lib/services/check.js +20 -12
- package/lib/services/check.js.map +1 -1
- package/lib/services/validate.d.ts +19 -0
- package/lib/services/validate.js +43 -0
- package/lib/services/validate.js.map +1 -0
- package/messages/ps.validate.md +29 -0
- package/oclif.manifest.json +74 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -164,7 +164,7 @@ Every run checks the files first. `check` runs the offline checks with no org, a
|
|
|
164
164
|
| Situation | Checked by | Severity | Result |
|
|
165
165
|
| --- | --- | :---: | --- |
|
|
166
166
|
| Same username key appears twice in one file | `check` (offline) | ❌ error | Rejected, the intent is ambiguous |
|
|
167
|
-
| Same target listed twice for a user
|
|
167
|
+
| Same target listed twice for a user | `check` (offline) | ⚠️ warning | Deduped |
|
|
168
168
|
| A user with no scopes, or an empty list | `check` (offline) | ⚠️ warning | Ignored as a no-op |
|
|
169
169
|
| Same user in two files with different targets | `check` (offline) | ✅ ok | Merged into one model, the point of slicing |
|
|
170
170
|
| Declared user, permission set, group, or license missing or not unique | `validate` (online) | ❌ error | Run fails before any change |
|
|
@@ -343,11 +343,12 @@ The `next` tag is selected whenever the version contains a hyphen, not by GitHub
|
|
|
343
343
|
|
|
344
344
|
## Architecture
|
|
345
345
|
|
|
346
|
-
The plugin is layered so every command reuses the same core. Commands stay thin, services hold the orchestration,
|
|
346
|
+
The plugin is layered so every command reuses the same core. Commands stay thin, services hold the orchestration, core holds the reusable primitives, and a thin adapter layer isolates the Salesforce SDK.
|
|
347
347
|
|
|
348
|
-
- **Commands** (`src/commands/ps/`): oclif only.
|
|
349
|
-
- **Services** (`src/services/`): one per command (`check` today, then `
|
|
350
|
-
- **Core** (`src/core/`): the reusable building blocks.
|
|
348
|
+
- **Commands** (`src/commands/ps/`): oclif only. They parse flags, construct the service (wiring in the org adapter when the command needs one), render output, and set the exit code.
|
|
349
|
+
- **Services** (`src/services/`): one per command (`check` and `validate` today, then `plan`, `apply`, `export`). Each is a class built from its dependencies and inputs, with a parameterless `run()` that turns the core into a command's behavior.
|
|
350
|
+
- **Core** (`src/core/`): the reusable building blocks. Pure, with no `@salesforce/*` imports, so every piece is unit-testable on its own.
|
|
351
|
+
- **Adapters** (`src/adapters/`): the boundary to the outside world. A port, the `OrgClient` interface, declares the org operations the app needs, and `ConnectionOrgClient` is the adapter that backs it with a Salesforce `Connection`. Services depend on the port rather than the SDK, so they test against a fake and stay free of connection detail.
|
|
351
352
|
|
|
352
353
|
| Core module | Responsibility |
|
|
353
354
|
| --- | --- |
|
|
@@ -356,9 +357,10 @@ The plugin is layered so every command reuses the same core. Commands stay thin,
|
|
|
356
357
|
| `parse` | File text to an object, with YAML and duplicate-key errors. |
|
|
357
358
|
| `normalize` | A validated file to canonical `(assignee, kind, target)` tuples, plus structural findings. |
|
|
358
359
|
| `load` | Expand globs, run parse then validate then normalize per file, and merge by union. |
|
|
360
|
+
| `resolve` | Plan the org queries that resolve declared references, each paired with a pure evaluator that turns the returned rows into findings. |
|
|
359
361
|
| `report` | Format and count findings. |
|
|
360
362
|
|
|
361
|
-
Commands are slices of one pipeline. `check` runs the offline **load** stage only.
|
|
363
|
+
Commands are slices of one pipeline. `check` runs the offline **load** stage only. `validate` adds **resolve**: it plans the SOQL from the loaded model, runs each query through the `OrgClient` port, and feeds the rows back to resolve's pure evaluators. The remaining online commands layer **fetch** (current org state), **diff** (desired vs actual), and **apply** (DML) on top, reusing load, resolve, report, model, and schema unchanged.
|
|
362
364
|
|
|
363
365
|
## License
|
|
364
366
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Connection } from '@salesforce/core';
|
|
2
|
+
import { Kind, OrgUser } from '../core/model.js';
|
|
3
|
+
/**
|
|
4
|
+
* Port: the org lookups the app needs, in domain terms. Services depend on this,
|
|
5
|
+
* not on @salesforce/core or SOQL, so they stay easy to test and persistence-ignorant.
|
|
6
|
+
*/
|
|
7
|
+
export interface OrgClient {
|
|
8
|
+
/** The users that exist in the org, among the given usernames. */
|
|
9
|
+
findUsers(usernames: string[]): Promise<OrgUser[]>;
|
|
10
|
+
/** The identifiers that exist in the org, among the given targets of one kind. */
|
|
11
|
+
findTargets(kind: Kind, names: string[]): Promise<string[]>;
|
|
12
|
+
}
|
|
13
|
+
/** Adapter backing OrgClient with a Salesforce Connection. autoFetchQuery pages past 2000 rows. */
|
|
14
|
+
export declare class ConnectionOrgClient implements OrgClient {
|
|
15
|
+
private readonly connection;
|
|
16
|
+
constructor(connection: Connection);
|
|
17
|
+
findUsers(usernames: string[]): Promise<OrgUser[]>;
|
|
18
|
+
findTargets(kind: Kind, names: string[]): Promise<string[]>;
|
|
19
|
+
private query;
|
|
20
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** SObject + naming field per kind. The Salesforce schema knowledge lives here, not in core. */
|
|
2
|
+
const TARGET_OBJECTS = {
|
|
3
|
+
permissionSet: { sobject: 'PermissionSet', field: 'Name' },
|
|
4
|
+
permissionSetGroup: { sobject: 'PermissionSetGroup', field: 'DeveloperName' },
|
|
5
|
+
permissionSetLicense: { sobject: 'PermissionSetLicense', field: 'DeveloperName' },
|
|
6
|
+
};
|
|
7
|
+
/** Escape a value for safe inclusion in a SOQL string literal. */
|
|
8
|
+
function soqlLiteral(value) {
|
|
9
|
+
return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
10
|
+
}
|
|
11
|
+
/** Build a comma-separated, quoted IN list from the values. */
|
|
12
|
+
function inList(values) {
|
|
13
|
+
return values.map((value) => `'${soqlLiteral(value)}'`).join(', ');
|
|
14
|
+
}
|
|
15
|
+
/** Adapter backing OrgClient with a Salesforce Connection. autoFetchQuery pages past 2000 rows. */
|
|
16
|
+
export class ConnectionOrgClient {
|
|
17
|
+
connection;
|
|
18
|
+
constructor(connection) {
|
|
19
|
+
this.connection = connection;
|
|
20
|
+
}
|
|
21
|
+
async findUsers(usernames) {
|
|
22
|
+
const records = await this.query(`SELECT Username, IsActive FROM User WHERE Username IN (${inList(usernames)})`);
|
|
23
|
+
return records.map((record) => ({ username: record.Username, isActive: record.IsActive }));
|
|
24
|
+
}
|
|
25
|
+
async findTargets(kind, names) {
|
|
26
|
+
const { sobject, field } = TARGET_OBJECTS[kind];
|
|
27
|
+
const records = await this.query(`SELECT ${field} FROM ${sobject} WHERE ${field} IN (${inList(names)})`);
|
|
28
|
+
return records.map((record) => record[field]);
|
|
29
|
+
}
|
|
30
|
+
async query(soql) {
|
|
31
|
+
const result = await this.connection.autoFetchQuery(soql);
|
|
32
|
+
return result.records;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=org-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"org-client.js","sourceRoot":"","sources":["../../src/adapters/org-client.ts"],"names":[],"mappings":"AAgBA,gGAAgG;AAChG,MAAM,cAAc,GAA+B;IAC/C,aAAa,EAAE,EAAE,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,EAAE;IAC1D,kBAAkB,EAAE,EAAE,OAAO,EAAE,oBAAoB,EAAE,KAAK,EAAE,eAAe,EAAE;IAC7E,oBAAoB,EAAE,EAAE,OAAO,EAAE,sBAAsB,EAAE,KAAK,EAAE,eAAe,EAAE;CACpF,CAAC;AAEF,kEAAkE;AAClE,SAAS,WAAW,CAAC,KAAa;IAC9B,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;AAC7D,CAAC;AAED,+DAA+D;AAC/D,SAAS,MAAM,CAAC,MAAgB;IAC5B,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACvE,CAAC;AAED,mGAAmG;AACnG,MAAM,OAAO,mBAAmB;IACQ;IAApC,YAAoC,UAAsB;QAAtB,eAAU,GAAV,UAAU,CAAY;IAAG,CAAC;IAEvD,KAAK,CAAC,SAAS,CAAC,SAAmB;QACtC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,KAAK,CAC5B,0DAA0D,MAAM,CAAC,SAAS,CAAC,GAAG,CACjF,CAAC;QAEF,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;IAC/F,CAAC;IAEM,KAAK,CAAC,WAAW,CAAC,IAAU,EAAE,KAAe;QAChD,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;QAChD,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,KAAK,CAC5B,UAAU,KAAK,SAAS,OAAO,UAAU,KAAK,QAAQ,MAAM,CAAC,KAAK,CAAC,GAAG,CACzE,CAAC;QAEF,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAClD,CAAC;IAEO,KAAK,CAAC,KAAK,CAAI,IAAY;QAC/B,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;QAC1D,OAAO,MAAM,CAAC,OAAyB,CAAC;IAC5C,CAAC;CACJ"}
|
package/lib/commands/ps/check.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
|
|
2
2
|
import { Messages } from '@salesforce/core';
|
|
3
|
-
import {
|
|
3
|
+
import { CheckService } from '../../services/check.js';
|
|
4
4
|
import { formatFindings } from '../../core/report.js';
|
|
5
5
|
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
|
|
6
6
|
const messages = Messages.loadMessages('sf-plugin-permission-sets', 'ps.check');
|
|
@@ -21,11 +21,12 @@ export default class Check extends SfCommand {
|
|
|
21
21
|
};
|
|
22
22
|
async run() {
|
|
23
23
|
const { flags } = await this.parse(Check);
|
|
24
|
-
const
|
|
24
|
+
const service = new CheckService(flags.file, flags.strict);
|
|
25
|
+
const result = await service.run();
|
|
25
26
|
for (const line of formatFindings(result.findings)) {
|
|
26
27
|
this.log(line);
|
|
27
28
|
}
|
|
28
|
-
const assignees = new Set(result.assignments.map((
|
|
29
|
+
const assignees = new Set(result.assignments.map((assignment) => assignment.assignee));
|
|
29
30
|
this.log('');
|
|
30
31
|
this.log(messages.getMessage('summary.counts', [String(result.errors), String(result.warnings)]));
|
|
31
32
|
if (result.failed) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check.js","sourceRoot":"","sources":["../../../src/commands/ps/check.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"check.js","sourceRoot":"","sources":["../../../src/commands/ps/check.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAGtD,QAAQ,CAAC,kCAAkC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC7D,MAAM,QAAQ,GAAG,QAAQ,CAAC,YAAY,CAAC,2BAA2B,EAAE,UAAU,CAAC,CAAC;AAShF,MAAM,CAAC,OAAO,OAAO,KAAM,SAAQ,SAAwB;IAChD,MAAM,CAAU,OAAO,GAAG,QAAQ,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;IACzD,MAAM,CAAU,WAAW,GAAG,QAAQ,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;IACjE,MAAM,CAAU,QAAQ,GAAG,QAAQ,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;IAE5D,MAAM,CAAU,KAAK,GAAG;QAC3B,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC;YACf,IAAI,EAAE,GAAG;YACT,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,oBAAoB,CAAC;YAClD,QAAQ,EAAE,IAAI;YACd,QAAQ,EAAE,IAAI;SACjB,CAAC;QACF,MAAM,EAAE,KAAK,CAAC,OAAO,CAAC;YAClB,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,sBAAsB,CAAC;SACvD,CAAC;KACL,CAAC;IAEK,KAAK,CAAC,GAAG;QACZ,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAE1C,MAAM,OAAO,GAAG,IAAI,YAAY,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;QAC3D,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE,CAAC;QAEnC,KAAK,MAAM,IAAI,IAAI,cAAc,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;YACjD,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACnB,CAAC;QAED,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC;QAEvF,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,gBAAgB,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QAElG,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;YAChB,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;YACrB,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;gBACtB,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,cAAc,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;YACjE,CAAC;QACL,CAAC;QAED,OAAO;YACH,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM;YAC1B,KAAK,EAAE,SAAS,CAAC,IAAI;YACrB,WAAW,EAAE,MAAM,CAAC,WAAW,CAAC,MAAM;YACtC,QAAQ,EAAE,MAAM,CAAC,QAAQ;SAC5B,CAAC;IACN,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { SfCommand } from '@salesforce/sf-plugins-core';
|
|
2
|
+
import { Finding } from '../../core/model.js';
|
|
3
|
+
export type PsValidateResult = {
|
|
4
|
+
files: number;
|
|
5
|
+
users: number;
|
|
6
|
+
assignments: number;
|
|
7
|
+
findings: Finding[];
|
|
8
|
+
};
|
|
9
|
+
export default class Validate extends SfCommand<PsValidateResult> {
|
|
10
|
+
static readonly summary: string;
|
|
11
|
+
static readonly description: string;
|
|
12
|
+
static readonly examples: string[];
|
|
13
|
+
static readonly flags: {
|
|
14
|
+
'target-org': import("@oclif/core/interfaces").OptionFlag<import("@salesforce/core").Org, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
'api-version': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
file: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
};
|
|
18
|
+
run(): Promise<PsValidateResult>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
|
|
2
|
+
import { Messages } from '@salesforce/core';
|
|
3
|
+
import { ConnectionOrgClient } from '../../adapters/org-client.js';
|
|
4
|
+
import { ValidateService } from '../../services/validate.js';
|
|
5
|
+
import { formatFindings } from '../../core/report.js';
|
|
6
|
+
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
|
|
7
|
+
const messages = Messages.loadMessages('sf-plugin-permission-sets', 'ps.validate');
|
|
8
|
+
export default class Validate extends SfCommand {
|
|
9
|
+
static summary = messages.getMessage('summary');
|
|
10
|
+
static description = messages.getMessage('description');
|
|
11
|
+
static examples = messages.getMessages('examples');
|
|
12
|
+
static flags = {
|
|
13
|
+
'target-org': Flags.requiredOrg(),
|
|
14
|
+
'api-version': Flags.orgApiVersion(),
|
|
15
|
+
file: Flags.string({
|
|
16
|
+
char: 'f',
|
|
17
|
+
summary: messages.getMessage('flags.file.summary'),
|
|
18
|
+
required: true,
|
|
19
|
+
multiple: true,
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
async run() {
|
|
23
|
+
const { flags } = await this.parse(Validate);
|
|
24
|
+
const connection = flags['target-org'].getConnection(flags['api-version']);
|
|
25
|
+
const orgClient = new ConnectionOrgClient(connection);
|
|
26
|
+
const service = new ValidateService(orgClient, flags.file);
|
|
27
|
+
const result = await service.run();
|
|
28
|
+
for (const line of formatFindings(result.findings)) {
|
|
29
|
+
this.log(line);
|
|
30
|
+
}
|
|
31
|
+
const assignees = new Set(result.assignments.map((assignment) => assignment.assignee));
|
|
32
|
+
this.log('');
|
|
33
|
+
this.log(messages.getMessage('summary.counts', [String(result.errors), String(result.warnings)]));
|
|
34
|
+
if (result.failed) {
|
|
35
|
+
process.exitCode = 1;
|
|
36
|
+
if (!this.jsonEnabled()) {
|
|
37
|
+
this.error(messages.getMessage('error.failed'), { exit: 1 });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
files: result.files.length,
|
|
42
|
+
users: assignees.size,
|
|
43
|
+
assignments: result.assignments.length,
|
|
44
|
+
findings: result.findings,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../../../src/commands/ps/validate.ts"],"names":[],"mappings":"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"}
|
package/lib/core/load.d.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
/** Process one file's text through parse, validate, and normalize. Pure, no disk. */
|
|
3
|
-
export declare function checkContent(text: string, file: string): {
|
|
4
|
-
assignments: DesiredAssignment[];
|
|
5
|
-
findings: Finding[];
|
|
6
|
-
};
|
|
1
|
+
import { LoadResult } from './model.js';
|
|
7
2
|
/** Expand the globs, read every matched file, and merge into one model by union. */
|
|
8
3
|
export declare function loadFiles(patterns: string[]): Promise<LoadResult>;
|
package/lib/core/load.js
CHANGED
|
@@ -4,7 +4,7 @@ import { parseFile } from './parse.js';
|
|
|
4
4
|
import { validateFile } from './schema.js';
|
|
5
5
|
import { normalize } from './normalize.js';
|
|
6
6
|
/** Process one file's text through parse, validate, and normalize. Pure, no disk. */
|
|
7
|
-
|
|
7
|
+
function checkContent(text, file) {
|
|
8
8
|
const parsed = parseFile(text, file);
|
|
9
9
|
if (!parsed.data) {
|
|
10
10
|
return { assignments: [], findings: parsed.findings };
|
|
@@ -40,12 +40,12 @@ export async function loadFiles(patterns) {
|
|
|
40
40
|
}
|
|
41
41
|
const seen = new Set();
|
|
42
42
|
const assignments = [];
|
|
43
|
-
for (const
|
|
44
|
-
const dedupeKey = `${
|
|
43
|
+
for (const assignment of collected) {
|
|
44
|
+
const dedupeKey = `${assignment.assignee} ${assignment.kind} ${assignment.target}`;
|
|
45
45
|
if (seen.has(dedupeKey))
|
|
46
46
|
continue;
|
|
47
47
|
seen.add(dedupeKey);
|
|
48
|
-
assignments.push(
|
|
48
|
+
assignments.push(assignment);
|
|
49
49
|
}
|
|
50
50
|
return { files, assignments, findings };
|
|
51
51
|
}
|
package/lib/core/load.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load.js","sourceRoot":"","sources":["../../src/core/load.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAG3C,qFAAqF;AACrF,
|
|
1
|
+
{"version":3,"file":"load.js","sourceRoot":"","sources":["../../src/core/load.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAG3C,qFAAqF;AACrF,SAAS,YAAY,CAAC,IAAY,EAAE,IAAY;IAC5C,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACrC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QACf,OAAO,EAAE,WAAW,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC;IAC1D,CAAC;IAED,MAAM,SAAS,GAAG,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAClD,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;QAClB,OAAO,EAAE,WAAW,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,GAAG,MAAM,CAAC,QAAQ,EAAE,GAAG,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC;IACtF,CAAC;IAED,MAAM,UAAU,GAAG,SAAS,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACnD,OAAO;QACH,WAAW,EAAE,UAAU,CAAC,WAAW;QACnC,QAAQ,EAAE,CAAC,GAAG,MAAM,CAAC,QAAQ,EAAE,GAAG,SAAS,CAAC,QAAQ,EAAE,GAAG,UAAU,CAAC,QAAQ,CAAC;KAChF,CAAC;AACN,CAAC;AAED,oFAAoF;AACpF,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,QAAkB;IAC9C,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC;IACrC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrB,OAAO;YACH,KAAK;YACL,WAAW,EAAE,EAAE;YACf,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,qBAAqB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;SACxG,CAAC;IACN,CAAC;IAED,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,MAAM,SAAS,GAAwB,EAAE,CAAC;IAC1C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACvB,4CAA4C;QAC5C,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC1C,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACrC,QAAQ,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC/B,SAAS,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,WAAW,CAAC,CAAC;IACvC,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,WAAW,GAAwB,EAAE,CAAC;IAE5C,KAAK,MAAM,UAAU,IAAI,SAAS,EAAE,CAAC;QACjC,MAAM,SAAS,GAAG,GAAG,UAAU,CAAC,QAAQ,IAAI,UAAU,CAAC,IAAI,IAAI,UAAU,CAAC,MAAM,EAAE,CAAC;QACnF,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC;YAAE,SAAS;QAClC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACpB,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACjC,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC;AAC5C,CAAC"}
|
package/lib/core/model.d.ts
CHANGED
|
@@ -4,6 +4,11 @@ export type DesiredAssignment = {
|
|
|
4
4
|
kind: Kind;
|
|
5
5
|
target: string;
|
|
6
6
|
};
|
|
7
|
+
/** A user as it exists in the org, in domain terms (no SObject field names). */
|
|
8
|
+
export type OrgUser = {
|
|
9
|
+
username: string;
|
|
10
|
+
isActive: boolean;
|
|
11
|
+
};
|
|
7
12
|
export type FindingLevel = 'error' | 'warning';
|
|
8
13
|
export type Finding = {
|
|
9
14
|
level: FindingLevel;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { DesiredAssignment, Finding, Kind, OrgUser } from './model.js';
|
|
2
|
+
export declare const KINDS: Kind[];
|
|
3
|
+
/** The distinct usernames assigned across all assignments. */
|
|
4
|
+
export declare function distinctAssignees(assignments: DesiredAssignment[]): string[];
|
|
5
|
+
/** The distinct targets of one kind across all assignments. */
|
|
6
|
+
export declare function distinctTargets(assignments: DesiredAssignment[], kind: Kind): string[];
|
|
7
|
+
/** Every declared user must exist in the org and be active. */
|
|
8
|
+
export declare function evaluateUsers(declared: string[], found: OrgUser[]): Finding[];
|
|
9
|
+
/**
|
|
10
|
+
* Every declared target of one kind must resolve to exactly one record in the
|
|
11
|
+
* org. `found` is the list of matching identifiers the org returned; matching is
|
|
12
|
+
* case-insensitive, mirroring how the org compares them.
|
|
13
|
+
*/
|
|
14
|
+
export declare function evaluateTargets(kind: Kind, declared: string[], found: string[]): Finding[];
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** Human label per kind, used in findings. Domain wording, not SObject names. */
|
|
2
|
+
const KIND_LABELS = {
|
|
3
|
+
permissionSet: 'permission set',
|
|
4
|
+
permissionSetGroup: 'permission set group',
|
|
5
|
+
permissionSetLicense: 'permission set license',
|
|
6
|
+
};
|
|
7
|
+
export const KINDS = Object.keys(KIND_LABELS);
|
|
8
|
+
function distinct(values) {
|
|
9
|
+
return [...new Set(values)];
|
|
10
|
+
}
|
|
11
|
+
/** The distinct usernames assigned across all assignments. */
|
|
12
|
+
export function distinctAssignees(assignments) {
|
|
13
|
+
return distinct(assignments.map((assignment) => assignment.assignee));
|
|
14
|
+
}
|
|
15
|
+
/** The distinct targets of one kind across all assignments. */
|
|
16
|
+
export function distinctTargets(assignments, kind) {
|
|
17
|
+
return distinct(assignments.filter((assignment) => assignment.kind === kind).map((assignment) => assignment.target));
|
|
18
|
+
}
|
|
19
|
+
/** Every declared user must exist in the org and be active. */
|
|
20
|
+
export function evaluateUsers(declared, found) {
|
|
21
|
+
const byName = new Map();
|
|
22
|
+
for (const user of found) {
|
|
23
|
+
byName.set(user.username.toLowerCase(), user);
|
|
24
|
+
}
|
|
25
|
+
const findings = [];
|
|
26
|
+
for (const username of declared) {
|
|
27
|
+
const user = byName.get(username.toLowerCase());
|
|
28
|
+
if (!user) {
|
|
29
|
+
findings.push({ level: 'error', code: 'USER_NOT_FOUND', message: `${username}: user not found in org` });
|
|
30
|
+
}
|
|
31
|
+
else if (!user.isActive) {
|
|
32
|
+
findings.push({ level: 'error', code: 'USER_INACTIVE', message: `${username}: user is inactive` });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return findings;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Every declared target of one kind must resolve to exactly one record in the
|
|
39
|
+
* org. `found` is the list of matching identifiers the org returned; matching is
|
|
40
|
+
* case-insensitive, mirroring how the org compares them.
|
|
41
|
+
*/
|
|
42
|
+
export function evaluateTargets(kind, declared, found) {
|
|
43
|
+
const label = KIND_LABELS[kind];
|
|
44
|
+
const counts = new Map();
|
|
45
|
+
for (const name of found) {
|
|
46
|
+
const key = name.toLowerCase();
|
|
47
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
48
|
+
}
|
|
49
|
+
const findings = [];
|
|
50
|
+
for (const target of declared) {
|
|
51
|
+
const count = counts.get(target.toLowerCase()) ?? 0;
|
|
52
|
+
if (count === 0) {
|
|
53
|
+
findings.push({
|
|
54
|
+
level: 'error',
|
|
55
|
+
code: 'TARGET_NOT_FOUND',
|
|
56
|
+
message: `${target}: ${label} not found in org`,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
else if (count > 1) {
|
|
60
|
+
findings.push({
|
|
61
|
+
level: 'error',
|
|
62
|
+
code: 'TARGET_AMBIGUOUS',
|
|
63
|
+
message: `${target}: ${label} is not unique in org`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return findings;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=resolve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/core/resolve.ts"],"names":[],"mappings":"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"}
|
package/lib/services/check.d.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import { DesiredAssignment, Finding } from '../core/model.js';
|
|
2
|
-
export type CheckOptions = {
|
|
3
|
-
files: string[];
|
|
4
|
-
strict: boolean;
|
|
5
|
-
};
|
|
6
2
|
export type CheckResult = {
|
|
7
3
|
files: string[];
|
|
8
4
|
assignments: DesiredAssignment[];
|
|
@@ -12,4 +8,9 @@ export type CheckResult = {
|
|
|
12
8
|
failed: boolean;
|
|
13
9
|
};
|
|
14
10
|
/** Offline check: load the files, validate them, and summarize the findings. */
|
|
15
|
-
export declare
|
|
11
|
+
export declare class CheckService {
|
|
12
|
+
private readonly files;
|
|
13
|
+
private readonly strict;
|
|
14
|
+
constructor(files: string[], strict: boolean);
|
|
15
|
+
run(): Promise<CheckResult>;
|
|
16
|
+
}
|
package/lib/services/check.js
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
import { loadFiles } from '../core/load.js';
|
|
2
2
|
import { countFindings } from '../core/report.js';
|
|
3
3
|
/** Offline check: load the files, validate them, and summarize the findings. */
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
warnings
|
|
14
|
-
failed
|
|
15
|
-
|
|
4
|
+
export class CheckService {
|
|
5
|
+
files;
|
|
6
|
+
strict;
|
|
7
|
+
constructor(files, strict) {
|
|
8
|
+
this.files = files;
|
|
9
|
+
this.strict = strict;
|
|
10
|
+
}
|
|
11
|
+
async run() {
|
|
12
|
+
const loaded = await loadFiles(this.files);
|
|
13
|
+
const { errors, warnings } = countFindings(loaded.findings);
|
|
14
|
+
const failed = errors > 0 || (this.strict && warnings > 0);
|
|
15
|
+
return {
|
|
16
|
+
files: loaded.files,
|
|
17
|
+
assignments: loaded.assignments,
|
|
18
|
+
findings: loaded.findings,
|
|
19
|
+
errors,
|
|
20
|
+
warnings,
|
|
21
|
+
failed,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
16
24
|
}
|
|
17
25
|
//# sourceMappingURL=check.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check.js","sourceRoot":"","sources":["../../src/services/check.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"check.js","sourceRoot":"","sources":["../../src/services/check.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAYlD,gFAAgF;AAChF,MAAM,OAAO,YAAY;IACe;IAAkC;IAAtE,YAAoC,KAAe,EAAmB,MAAe;QAAjD,UAAK,GAAL,KAAK,CAAU;QAAmB,WAAM,GAAN,MAAM,CAAS;IAAG,CAAC;IAElF,KAAK,CAAC,GAAG;QACZ,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3C,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,aAAa,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC5D,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,QAAQ,GAAG,CAAC,CAAC,CAAC;QAE3D,OAAO;YACH,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,WAAW,EAAE,MAAM,CAAC,WAAW;YAC/B,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,MAAM;YACN,QAAQ;YACR,MAAM;SACT,CAAC;IACN,CAAC;CACJ"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { OrgClient } from '../adapters/org-client.js';
|
|
2
|
+
import { DesiredAssignment, Finding } from '../core/model.js';
|
|
3
|
+
export type ValidateResult = {
|
|
4
|
+
files: string[];
|
|
5
|
+
assignments: DesiredAssignment[];
|
|
6
|
+
findings: Finding[];
|
|
7
|
+
errors: number;
|
|
8
|
+
warnings: number;
|
|
9
|
+
failed: boolean;
|
|
10
|
+
};
|
|
11
|
+
/** Online validate: run the offline load, then resolve every reference against the org. */
|
|
12
|
+
export declare class ValidateService {
|
|
13
|
+
private readonly org;
|
|
14
|
+
private readonly files;
|
|
15
|
+
constructor(org: OrgClient, files: string[]);
|
|
16
|
+
run(): Promise<ValidateResult>;
|
|
17
|
+
/** Look every reference up in the org (in parallel) and evaluate the results. */
|
|
18
|
+
private resolve;
|
|
19
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { loadFiles } from '../core/load.js';
|
|
2
|
+
import { countFindings } from '../core/report.js';
|
|
3
|
+
import { KINDS, distinctAssignees, distinctTargets, evaluateUsers, evaluateTargets } from '../core/resolve.js';
|
|
4
|
+
/** Online validate: run the offline load, then resolve every reference against the org. */
|
|
5
|
+
export class ValidateService {
|
|
6
|
+
org;
|
|
7
|
+
files;
|
|
8
|
+
constructor(org, files) {
|
|
9
|
+
this.org = org;
|
|
10
|
+
this.files = files;
|
|
11
|
+
}
|
|
12
|
+
async run() {
|
|
13
|
+
const loaded = await loadFiles(this.files);
|
|
14
|
+
const online = await this.resolve(loaded.assignments);
|
|
15
|
+
const findings = [...loaded.findings, ...online];
|
|
16
|
+
const { errors, warnings } = countFindings(findings);
|
|
17
|
+
return {
|
|
18
|
+
files: loaded.files,
|
|
19
|
+
assignments: loaded.assignments,
|
|
20
|
+
findings,
|
|
21
|
+
errors,
|
|
22
|
+
warnings,
|
|
23
|
+
failed: errors > 0,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Look every reference up in the org (in parallel) and evaluate the results. */
|
|
27
|
+
async resolve(assignments) {
|
|
28
|
+
const tasks = [];
|
|
29
|
+
const usernames = distinctAssignees(assignments);
|
|
30
|
+
if (usernames.length > 0) {
|
|
31
|
+
tasks.push(this.org.findUsers(usernames).then((found) => evaluateUsers(usernames, found)));
|
|
32
|
+
}
|
|
33
|
+
for (const kind of KINDS) {
|
|
34
|
+
const targets = distinctTargets(assignments, kind);
|
|
35
|
+
if (targets.length > 0) {
|
|
36
|
+
tasks.push(this.org.findTargets(kind, targets).then((found) => evaluateTargets(kind, targets, found)));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const results = await Promise.all(tasks);
|
|
40
|
+
return results.flat();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../../src/services/validate.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAY/G,2FAA2F;AAC3F,MAAM,OAAO,eAAe;IACY;IAAiC;IAArE,YAAoC,GAAc,EAAmB,KAAe;QAAhD,QAAG,GAAH,GAAG,CAAW;QAAmB,UAAK,GAAL,KAAK,CAAU;IAAG,CAAC;IAEjF,KAAK,CAAC,GAAG;QACZ,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3C,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;QAEtD,MAAM,QAAQ,GAAG,CAAC,GAAG,MAAM,CAAC,QAAQ,EAAE,GAAG,MAAM,CAAC,CAAC;QACjD,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;QAErD,OAAO;YACH,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,WAAW,EAAE,MAAM,CAAC,WAAW;YAC/B,QAAQ;YACR,MAAM;YACN,QAAQ;YACR,MAAM,EAAE,MAAM,GAAG,CAAC;SACrB,CAAC;IACN,CAAC;IAED,iFAAiF;IACzE,KAAK,CAAC,OAAO,CAAC,WAAgC;QAClD,MAAM,KAAK,GAA8B,EAAE,CAAC;QAE5C,MAAM,SAAS,GAAG,iBAAiB,CAAC,WAAW,CAAC,CAAC;QACjD,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,aAAa,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;QAC/F,CAAC;QAED,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACvB,MAAM,OAAO,GAAG,eAAe,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;YACnD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACrB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,eAAe,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;YAC3G,CAAC;QACL,CAAC;QAED,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACzC,OAAO,OAAO,CAAC,IAAI,EAAE,CAAC;IAC1B,CAAC;CACJ"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# summary
|
|
2
|
+
|
|
3
|
+
Validate permission set assignment files against a target org.
|
|
4
|
+
|
|
5
|
+
# description
|
|
6
|
+
|
|
7
|
+
Run every offline check, then resolve each referenced user, permission set, group, and license against the org. Reports users that are missing or inactive and targets that are missing or not unique. Read-only: it queries the org but never changes it. Run it before plan or apply.
|
|
8
|
+
|
|
9
|
+
# flags.file.summary
|
|
10
|
+
|
|
11
|
+
YAML file or glob to validate. Repeatable.
|
|
12
|
+
|
|
13
|
+
# summary.counts
|
|
14
|
+
|
|
15
|
+
%s errors, %s warnings.
|
|
16
|
+
|
|
17
|
+
# error.failed
|
|
18
|
+
|
|
19
|
+
Validation found problems. See the output above.
|
|
20
|
+
|
|
21
|
+
# examples
|
|
22
|
+
|
|
23
|
+
- Validate every file under permissions against the dev org:
|
|
24
|
+
|
|
25
|
+
<%= config.bin %> <%= command.id %> --file "permissions/\*.yml" --target-org dev
|
|
26
|
+
|
|
27
|
+
- Validate specific files against a named org:
|
|
28
|
+
|
|
29
|
+
<%= config.bin %> <%= command.id %> --file permissions/sales.yml --file permissions/service.yml --target-org prod
|
package/oclif.manifest.json
CHANGED
|
@@ -107,7 +107,80 @@
|
|
|
107
107
|
"ps:info",
|
|
108
108
|
"info:ps"
|
|
109
109
|
]
|
|
110
|
+
},
|
|
111
|
+
"ps:validate": {
|
|
112
|
+
"aliases": [],
|
|
113
|
+
"args": {},
|
|
114
|
+
"description": "Run every offline check, then resolve each referenced user, permission set, group, and license against the org. Reports users that are missing or inactive and targets that are missing or not unique. Read-only: it queries the org but never changes it. Run it before plan or apply.",
|
|
115
|
+
"examples": [
|
|
116
|
+
"Validate every file under permissions against the dev org:\n<%= config.bin %> <%= command.id %> --file \"permissions/\\*.yml\" --target-org dev",
|
|
117
|
+
"Validate specific files against a named org:\n<%= config.bin %> <%= command.id %> --file permissions/sales.yml --file permissions/service.yml --target-org prod"
|
|
118
|
+
],
|
|
119
|
+
"flags": {
|
|
120
|
+
"json": {
|
|
121
|
+
"description": "Format output as json.",
|
|
122
|
+
"helpGroup": "GLOBAL",
|
|
123
|
+
"name": "json",
|
|
124
|
+
"allowNo": false,
|
|
125
|
+
"type": "boolean"
|
|
126
|
+
},
|
|
127
|
+
"flags-dir": {
|
|
128
|
+
"helpGroup": "GLOBAL",
|
|
129
|
+
"name": "flags-dir",
|
|
130
|
+
"summary": "Import flag values from a directory.",
|
|
131
|
+
"hasDynamicHelp": false,
|
|
132
|
+
"multiple": false,
|
|
133
|
+
"type": "option"
|
|
134
|
+
},
|
|
135
|
+
"target-org": {
|
|
136
|
+
"char": "o",
|
|
137
|
+
"name": "target-org",
|
|
138
|
+
"noCacheDefault": true,
|
|
139
|
+
"required": true,
|
|
140
|
+
"summary": "Username or alias of the target org. Not required if the `target-org` configuration variable is already set.",
|
|
141
|
+
"hasDynamicHelp": true,
|
|
142
|
+
"multiple": false,
|
|
143
|
+
"type": "option"
|
|
144
|
+
},
|
|
145
|
+
"api-version": {
|
|
146
|
+
"description": "Override the api version used for api requests made by this command",
|
|
147
|
+
"name": "api-version",
|
|
148
|
+
"hasDynamicHelp": false,
|
|
149
|
+
"multiple": false,
|
|
150
|
+
"type": "option"
|
|
151
|
+
},
|
|
152
|
+
"file": {
|
|
153
|
+
"char": "f",
|
|
154
|
+
"name": "file",
|
|
155
|
+
"required": true,
|
|
156
|
+
"summary": "YAML file or glob to validate. Repeatable.",
|
|
157
|
+
"hasDynamicHelp": false,
|
|
158
|
+
"multiple": true,
|
|
159
|
+
"type": "option"
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
"hasDynamicHelp": true,
|
|
163
|
+
"hiddenAliases": [],
|
|
164
|
+
"id": "ps:validate",
|
|
165
|
+
"pluginAlias": "sf-plugin-permission-sets",
|
|
166
|
+
"pluginName": "sf-plugin-permission-sets",
|
|
167
|
+
"pluginType": "core",
|
|
168
|
+
"strict": true,
|
|
169
|
+
"summary": "Validate permission set assignment files against a target org.",
|
|
170
|
+
"enableJsonFlag": true,
|
|
171
|
+
"isESM": true,
|
|
172
|
+
"relativePath": [
|
|
173
|
+
"lib",
|
|
174
|
+
"commands",
|
|
175
|
+
"ps",
|
|
176
|
+
"validate.js"
|
|
177
|
+
],
|
|
178
|
+
"aliasPermutations": [],
|
|
179
|
+
"permutations": [
|
|
180
|
+
"ps:validate",
|
|
181
|
+
"validate:ps"
|
|
182
|
+
]
|
|
110
183
|
}
|
|
111
184
|
},
|
|
112
|
-
"version": "0.0.0-dev.
|
|
185
|
+
"version": "0.0.0-dev.17"
|
|
113
186
|
}
|
package/package.json
CHANGED