Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Thanks for your interest in contributing to Simply! This document covers the rep

## Repository Structure

This repository is a Lerna monorepo containing twelve Salesforce CLI plugins, plus four internal libraries. Every package has its own `CONTRIBUTING.md` covering what's specific to it — read this file first, then that one.
This repository is a Lerna monorepo containing thirteen Salesforce CLI plugins, plus four internal libraries. Every package has its own `CONTRIBUTING.md` covering what's specific to it — read this file first, then that one.

| Package | Description | Bundled into `simply`? |
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
Expand All @@ -25,6 +25,7 @@ This repository is a Lerna monorepo containing twelve Salesforce CLI plugins, pl
| [`@simplysf/simply-community`](packages/simply-community) | Salesforce Communities commands | ✅ |
| [`@simplysf/simply-data`](packages/simply-data) | File upload/download commands | ✅ |
| [`@simplysf/simply-document`](packages/simply-document) | Documentation generation commands | ✅ |
| [`@simplysf/simply-flow`](packages/simply-flow) | Flow commands | ✅ |
| [`@simplysf/simply-package`](packages/simply-package) | Package dependency management commands | ✅ |
| [`@simplysf/simply-permissions`](packages/simply-permissions) | Permissions commands | ✅ |
| [`@simplysf/simply-project`](packages/simply-project) | Salesforce project commands | ✅ |
Expand Down
27 changes: 27 additions & 0 deletions docs/design/0013-flow-and-permission-set-assignment-cleanup.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,30 @@ Parses a `<Package><types><name>...</name><members>...</members></types>...</Pac
- **Should `simply-cicd`'s `pre-destructive`/`post-destructive` stages eventually call these commands automatically**, instead of requiring a project's own `bin/preDestructive.sh` to shell out to them? Left as future work — this doc only makes the commands exist and reusable; wiring them into the stage pipeline as a built-in, no-`bin/`-script-required option is a separate decision with its own tradeoffs (implicit behavior vs. explicit opt-in).
- **Whether the Tooling API supports a bulk multi-ID `destroy()`** for `Flow` (avoiding one round trip per version) is an implementation-time investigation, not a design decision — the original script loops one at a time; if a bulk form exists, use it, but the command's external behavior (failure-collection, structured result) is unaffected either way.
- **Namespaced/managed-package Flow or PermissionSet names** — out of scope, no different than any other command in this repo; revisit if a real project needs it.

## Implementation notes (post-implementation)

A few places where implementing this taught something the design above didn't anticipate:

- **A failed `SaveResult` carries `id: undefined`, not the id that failed.** jsforce-node's `SaveResult`
type is a discriminated union — `{ success: true; id: string; errors: never[] }` or
`{ success: false; id?: undefined; errors: SaveError[] }` — so a failure branch has no `id` to attribute
a failure to. `assignment delete`'s chunked `PermissionSetAssignment` delete pairs each response entry
with the request chunk's id by array index instead (`connection.sobject(...).delete(idChunk)`'s response
array is positional against the request array), rather than reading `result.id`. This wasn't a problem
for `flow delete`/`version prune`'s single-id `Flow`/`FlowDefinition` calls, since those never need a
result-reported id — the developer name is already known from the calling loop's own variable.
- **Resolved the "does the Tooling API support a bulk multi-ID `destroy()`" open question, partially.**
jsforce's `destroy()`/`delete()` doesn't special-case the Tooling API — `connection.tooling.sobject('Flow').destroy([id1, id2, ...])`
would issue a single `DELETE .../tooling/composite/sobjects?ids=...` request the same way the standard
API's composite delete works. What's unverified is whether Salesforce's server-side Tooling API REST
layer actually accepts that composite-delete path for objects like `Flow` — nothing in this change
exercises a real org, so this is a client-library capability, not a confirmed server capability. Given
that uncertainty, `flow delete` and `version prune` both keep the original scripts' one-Flow-version-at-a-time
loop rather than risk an unverified bulk endpoint. Worth revisiting with a NUT against a real org if
per-version round trips become a real performance concern.
- **`targetOrgFlags`/`requireConnection` from `@simplysf/simply-plugin-kit`** (already used by
`simply-permissions`) were used for all three commands' `--target-org`/`--api-version` instead of each
command hand-rolling `Flags.optionalOrg({ char: 'o' })` plus its own null-connection check — not a
divergence from the design doc's Behavior tables in practice, since `Flags.requiredOrg()` already
defaults to the `-o` short char.
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const allPackages = [
'packages/simply-core',
'packages/simply-data',
'packages/simply-document',
'packages/simply-flow',
'packages/simply-package',
'packages/simply-permissions',
'packages/simply-plugin-kit',
Expand Down
1 change: 1 addition & 0 deletions packages/simply-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@salesforce/kit": "^3.2.6",
"csv-parse": "^7.0.2",
"csv-stringify": "^6.8.3",
"fast-xml-parser": "^5.11.0",
"undici": "^8.10.0"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions packages/simply-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,4 @@ export {
type ResolvePackageNamesByApiNameOptions,
type ResolvePackageNamesOptions,
} from './metadata/packageAttribution.js';
export { readPackageManifestMembers } from './metadata/packageManifest.js';
63 changes: 63 additions & 0 deletions packages/simply-core/src/metadata/packageManifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright (c) 2026, Clay Chipps.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { XMLParser } from 'fast-xml-parser';

type RawPackageManifestType = {
name: string;
members?: string | string[];
};

type RawPackageManifestXml = {
Package?: {
types?: RawPackageManifestType | RawPackageManifestType[];
};
};

/**
* Read the `<members>` of a `<types>` block matching `typeName` out of a `package.xml`/
* `destructiveChanges.xml`-shaped document.
*
* Both files share the same `<Package><types><name/><members/></types></Package>` shape, and
* `fast-xml-parser` collapses a single `<types>` or `<members>` element to a bare object/string
* rather than a one-element array — normalized here the same way `customMetadataXml.ts`'s
* `extractValues` normalizes the analogous `<values>` shape for `CustomMetadata` XML.
*
* @param xmlContent - The manifest file's raw XML text.
* @param typeName - The `<name>` to look up (e.g. `'Flow'`, `'PermissionSet'`).
* @returns The matching type's members, or `[]` if `typeName` isn't present in the file at all —
* matching how a destructive-changes-driven caller treats "nothing of this type" as a no-op, not
* an error.
*/
export function readPackageManifestMembers(xmlContent: string, typeName: string): string[] {
const parsed = new XMLParser().parse(xmlContent) as RawPackageManifestXml;

const rawTypes = parsed.Package?.types;
const types = rawTypes === undefined ? [] : Array.isArray(rawTypes) ? rawTypes : [rawTypes];

const members: string[] = [];
for (const type of types) {
if (type.name !== typeName) {
continue;
}
if (type.members === undefined) {
continue;
}
members.push(...(Array.isArray(type.members) ? type.members : [type.members]));
}

return members;
}
50 changes: 50 additions & 0 deletions packages/simply-core/test/metadata/packageManifest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright (c) 2026, Clay Chipps.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { describe, expect, it } from 'vitest';
import { readPackageManifestMembers } from '../../src/metadata/packageManifest.js';

const XML_HEADER = '<?xml version="1.0" encoding="UTF-8"?>\n<Package xmlns="http://soap.sforce.com/2006/04/metadata">';

describe('readPackageManifestMembers', () => {
it('normalizes a single <types> block with a single <members> element', () => {
const xml = `${XML_HEADER}\n <types>\n <members>My_Flow</members>\n <name>Flow</name>\n </types>\n <version>62.0</version>\n</Package>\n`;

expect(readPackageManifestMembers(xml, 'Flow')).to.deep.equal(['My_Flow']);
});

it('normalizes multiple <types> blocks, each with multiple <members>', () => {
const xml = `${XML_HEADER}\n <types>\n <members>My_Flow</members>\n <members>Another_Flow</members>\n <name>Flow</name>\n </types>\n <types>\n <members>My_Permission_Set</members>\n <members>Another_Permission_Set</members>\n <name>PermissionSet</name>\n </types>\n <version>62.0</version>\n</Package>\n`;

expect(readPackageManifestMembers(xml, 'Flow')).to.deep.equal(['My_Flow', 'Another_Flow']);
expect(readPackageManifestMembers(xml, 'PermissionSet')).to.deep.equal([
'My_Permission_Set',
'Another_Permission_Set',
]);
});

it('returns an empty array when the requested type is not present', () => {
const xml = `${XML_HEADER}\n <types>\n <members>My_Flow</members>\n <name>Flow</name>\n </types>\n <version>62.0</version>\n</Package>\n`;

expect(readPackageManifestMembers(xml, 'PermissionSetGroup')).to.deep.equal([]);
});

it('returns an empty array for a Package with no <types> at all', () => {
const xml = `${XML_HEADER}\n <version>62.0</version>\n</Package>\n`;

expect(readPackageManifestMembers(xml, 'Flow')).to.deep.equal([]);
});
});
51 changes: 51 additions & 0 deletions packages/simply-flow/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# -- CLEAN
tmp/
# use yarn by default, so ignore npm
package-lock.json

# never checkin npm config
.npmrc

# debug logs
npm-error.log
yarn-error.log

# history extension
.history

# compile source
lib

# test artifacts
*xunit.xml
*checkstyle.xml
*unitcoverage
.nyc_output
coverage
test_session*

# generated docs
docs

# ignore sfdx-trust files
*.tgz
*.sig
package.json.bak.


npm-shrinkwrap.json
oclif.manifest.json
oclif.lock

# -- CLEAN ALL
*.tsbuildinfo
.eslintcache
.wireit
node_modules

# --
# put files here you don't want cleaned with sf-clean

# os specific files
.DS_Store
.idea
61 changes: 61 additions & 0 deletions packages/simply-flow/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Contributing to @simplysf/simply-flow

Utilities for working with Flows. This package is part of the [`simply-node`](https://github.com/SimplySF/simply-node) monorepo.

**Start with the [root CONTRIBUTING.md](https://github.com/SimplySF/simply-node/blob/main/CONTRIBUTING.md).** It covers repository structure, environment setup, commit conventions, versioning and publishing, CI, git hooks, and the pull request process — all of which apply here. This file covers only what is specific to this package.

## Working on this package

Run from this directory to target just this package:

```sh
pnpm run build # compile + lint + regenerate command-snapshot.json
pnpm test # the full gate CI runs
pnpm run test:only # just the unit tests, skipping lint and the doc gates
pnpm run lint
```

## Trying a command locally

Run this package's dev binary without installing it into the Salesforce CLI:

```sh
./bin/dev.js --help # macOS/Linux
./bin/dev.cmd --help # Windows
```

Or link it so `sf` picks it up from anywhere:

```sh
sf plugins link .
```

## Command help text

Summaries, descriptions, examples, and error messages live in [`messages/`](messages), not in the command source. Edit the relevant `messages/*.md` file, then regenerate the README command reference:

```sh
pnpm run readme
```

Commit the regenerated `README.md`. The docs site derives its command reference pages from it, so a stale README means stale published docs.

## Command snapshot

`command-snapshot.json` records every command and flag so that accidental breaking changes surface in review. It regenerates as part of `pnpm run build` — commit whatever changes. CI re-verifies with `git diff --exit-code`, so a stale snapshot fails the build.

> **If you add, remove, or rename a flag here, also rebuild [`packages/simply`](https://github.com/SimplySF/simply-node/tree/main/packages/simply)'s snapshot.** `@simplysf/simply-flow` is bundled into the orchestrator plugin, so its aggregated snapshot carries these flags too. The orchestrator's wireit cache only watches `packages/simply/src/**/*.ts`, so a plain `pnpm run build` there reports cached success without regenerating anything — the drift only surfaces in CI. Force it:

> ```sh
> cd ../simply
> node --loader ts-node/esm --no-warnings=ExperimentalWarning ./bin/dev.js snapshot:generate
> npx prettier --write command-snapshot.json
> ```

## Tests

No pull request is accepted without tests covering the change. Tests live in [`test/`](test), mirroring the `src/` layout, and run under [Vitest](https://vitest.dev/).

## Reporting issues

Please [open an issue](https://github.com/SimplySF/simply-node/issues) rather than sending a pull request for anything non-trivial without prior discussion.
Loading