sf-exit 0.1.0__tar.gz
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.
- sf_exit-0.1.0/.gitignore +24 -0
- sf_exit-0.1.0/CHANGELOG.md +28 -0
- sf_exit-0.1.0/LICENSE +21 -0
- sf_exit-0.1.0/PKG-INFO +327 -0
- sf_exit-0.1.0/README.md +277 -0
- sf_exit-0.1.0/pyproject.toml +77 -0
- sf_exit-0.1.0/src/sf_exit/__init__.py +3 -0
- sf_exit-0.1.0/src/sf_exit/__main__.py +4 -0
- sf_exit-0.1.0/src/sf_exit/auth.py +190 -0
- sf_exit-0.1.0/src/sf_exit/cli.py +498 -0
- sf_exit-0.1.0/src/sf_exit/client.py +446 -0
- sf_exit-0.1.0/src/sf_exit/discovery.py +199 -0
- sf_exit-0.1.0/src/sf_exit/exporter.py +631 -0
- sf_exit-0.1.0/src/sf_exit/loader.py +247 -0
- sf_exit-0.1.0/src/sf_exit/schema.py +134 -0
- sf_exit-0.1.0/src/sf_exit/utils.py +52 -0
- sf_exit-0.1.0/src/sf_exit/verify.py +122 -0
- sf_exit-0.1.0/tests/__init__.py +0 -0
- sf_exit-0.1.0/tests/fake_org.py +402 -0
- sf_exit-0.1.0/tests/test_cli.py +55 -0
- sf_exit-0.1.0/tests/test_export.py +270 -0
- sf_exit-0.1.0/tests/test_units.py +137 -0
sf_exit-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*.egg-info/
|
|
4
|
+
.eggs/
|
|
5
|
+
build/
|
|
6
|
+
dist/
|
|
7
|
+
.venv/
|
|
8
|
+
venv/
|
|
9
|
+
.pytest_cache/
|
|
10
|
+
.ruff_cache/
|
|
11
|
+
.coverage
|
|
12
|
+
htmlcov/
|
|
13
|
+
.DS_Store
|
|
14
|
+
.idea/
|
|
15
|
+
.vscode/
|
|
16
|
+
|
|
17
|
+
# Exports contain customer data: never commit them by accident
|
|
18
|
+
sf-export/
|
|
19
|
+
export/
|
|
20
|
+
*.duckdb
|
|
21
|
+
*.db
|
|
22
|
+
*.parquet
|
|
23
|
+
server.key
|
|
24
|
+
.env
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-11-14
|
|
8
|
+
|
|
9
|
+
First public release.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `sf-exit export`: every queryable object to CSV, via Bulk API 2.0 (large objects) or REST (small objects), with automatic fallback between them.
|
|
13
|
+
- Downloads of ContentVersion, Attachment and Document bodies, verified against Salesforce's MD5 checksums, plus `files/index.csv`.
|
|
14
|
+
- `ContentDocumentLink` export (file ↔ record links), which Salesforce won't return without a filter.
|
|
15
|
+
- Resumable exports (`.sf-exit/state.json`), `--include-deleted`, include/exclude globs.
|
|
16
|
+
- Schema output: per-object describe JSON, DDL for Postgres/DuckDB/SQLite, a Mermaid ER diagram, `manifest.json` and `REPORT.md`.
|
|
17
|
+
- `sf-exit load` into SQLite, DuckDB, Postgres and Parquet, with typed columns.
|
|
18
|
+
- `sf-exit verify` (offline row counts and SHA-256, and `--live` comparison against the org).
|
|
19
|
+
- `sf-exit plan` (dry run with API-limit estimate) and `sf-exit doctor` (permission check).
|
|
20
|
+
- Login via Salesforce CLI, access token, OAuth client credentials, JWT bearer or username-password.
|
|
21
|
+
- GitHub Action and Docker image.
|
|
22
|
+
- Composite batch requests: describes and small-object queries are fetched 25 per API call.
|
|
23
|
+
- Knowledge articles (`KnowledgeArticleVersion`, `*__kav`) are exported per `PublishStatus`.
|
|
24
|
+
- Objects that Salesforce refuses to export unfiltered are reported as skipped (with the reason) instead of failing the run. Permission and limit errors still count as failures.
|
|
25
|
+
- Resuming with different options (for example `--include-deleted`) starts a clean export instead of mixing results.
|
|
26
|
+
- Bulk jobs that don't finish within 6 hours are aborted and reported, instead of being polled forever.
|
|
27
|
+
|
|
28
|
+
[0.1.0]: https://github.com/akhsterminator/sf-exit/releases/tag/v0.1.0
|
sf_exit-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 akhsterminator and sf-exit contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
sf_exit-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sf-exit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Leave Salesforce with all your data. One command exports every object, file and attachment to CSV, then loads it into SQLite, DuckDB, Postgres or Parquet.
|
|
5
|
+
Project-URL: Homepage, https://github.com/akhsterminator/sf-exit
|
|
6
|
+
Project-URL: Documentation, https://github.com/akhsterminator/sf-exit#readme
|
|
7
|
+
Project-URL: Issues, https://github.com/akhsterminator/sf-exit/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/akhsterminator/sf-exit/blob/main/CHANGELOG.md
|
|
9
|
+
Author: sf-exit contributors
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: backup,bulk-api,crm,data-migration,data-portability,duckdb,etl,export,migration,parquet,postgres,salesforce,sqlite
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: Information Technology
|
|
17
|
+
Classifier: Intended Audience :: System Administrators
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
26
|
+
Classifier: Topic :: Database
|
|
27
|
+
Classifier: Topic :: System :: Archiving :: Backup
|
|
28
|
+
Classifier: Topic :: Utilities
|
|
29
|
+
Requires-Python: >=3.10
|
|
30
|
+
Requires-Dist: click>=8.1
|
|
31
|
+
Requires-Dist: requests>=2.28
|
|
32
|
+
Requires-Dist: rich>=13.0
|
|
33
|
+
Provides-Extra: all
|
|
34
|
+
Requires-Dist: duckdb>=1.1; extra == 'all'
|
|
35
|
+
Requires-Dist: psycopg[binary]>=3.1; extra == 'all'
|
|
36
|
+
Requires-Dist: pyjwt[crypto]>=2.8; extra == 'all'
|
|
37
|
+
Provides-Extra: dev
|
|
38
|
+
Requires-Dist: duckdb>=1.1; extra == 'dev'
|
|
39
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
40
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
41
|
+
Provides-Extra: duckdb
|
|
42
|
+
Requires-Dist: duckdb>=1.1; extra == 'duckdb'
|
|
43
|
+
Provides-Extra: jwt
|
|
44
|
+
Requires-Dist: pyjwt[crypto]>=2.8; extra == 'jwt'
|
|
45
|
+
Provides-Extra: parquet
|
|
46
|
+
Requires-Dist: duckdb>=1.1; extra == 'parquet'
|
|
47
|
+
Provides-Extra: postgres
|
|
48
|
+
Requires-Dist: psycopg[binary]>=3.1; extra == 'postgres'
|
|
49
|
+
Description-Content-Type: text/markdown
|
|
50
|
+
|
|
51
|
+
<div align="center">
|
|
52
|
+
|
|
53
|
+
# sf-exit
|
|
54
|
+
|
|
55
|
+
**Leave Salesforce with all of your data.**
|
|
56
|
+
|
|
57
|
+
One command exports every record, file and attachment from a Salesforce org,<br>
|
|
58
|
+
then loads it into **SQLite, DuckDB, Postgres or Parquet**, with every Id and relationship kept.
|
|
59
|
+
|
|
60
|
+
[](https://github.com/akhsterminator/sf-exit/actions/workflows/ci.yml)
|
|
61
|
+
[](https://pypi.org/project/sf-exit/)
|
|
62
|
+
[](https://pypi.org/project/sf-exit/)
|
|
63
|
+
[](LICENSE)
|
|
64
|
+
[](https://github.com/akhsterminator/sf-exit/stargazers)
|
|
65
|
+
|
|
66
|
+
[Quick start](#quick-start) · [What you get](#what-you-get) · [Why](#why-sf-exit) · [Nightly backups](#nightly-backups-with-github-actions) · [FAQ](#faq) · [Roadmap](#roadmap)
|
|
67
|
+
|
|
68
|
+
<img src="docs/demo.svg" alt="sf-exit exporting a Salesforce org and loading it into DuckDB" width="820">
|
|
69
|
+
|
|
70
|
+
<sub>A real run, recorded against the simulated org that the test suite uses (<code>scripts/record_demo.py</code>).</sub>
|
|
71
|
+
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Quick start
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pipx install sf-exit # or: pip install sf-exit / uvx sf-exit
|
|
80
|
+
sf org login web --alias my-org # log in once with the Salesforce CLI
|
|
81
|
+
sf-exit export --org my-org -d ./export # every object + every file
|
|
82
|
+
sf-exit load ./export --to duckdb:///salesforce.duckdb
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
That's it. There's no connected app to configure and no Salesforce setup to change: sf-exit reuses your [Salesforce CLI](https://developer.salesforce.com/tools/salesforcecli) login. Other login methods (access token, JWT, OAuth client credentials, username + password) are covered [below](#authentication).
|
|
86
|
+
|
|
87
|
+
Not sure what you're in for? Run a **dry run** first:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
sf-exit plan --org my-org # objects, row counts, file storage, API calls needed vs. your limits
|
|
91
|
+
sf-exit doctor --org my-org # checks you have "View All Data" and "Query All Files"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## What you get
|
|
95
|
+
|
|
96
|
+
```text
|
|
97
|
+
export/
|
|
98
|
+
├── data/
|
|
99
|
+
│ ├── Account.csv ← one CSV per object, every field, original 18-char Ids
|
|
100
|
+
│ ├── Contact.csv
|
|
101
|
+
│ ├── Invoice__c.csv ← custom objects too
|
|
102
|
+
│ ├── ContentDocumentLink.csv ← which file belongs to which record
|
|
103
|
+
│ └── … (every queryable object: history, tasks, emails, chatter, custom metadata…)
|
|
104
|
+
├── files/
|
|
105
|
+
│ ├── ContentVersion/<ContentDocumentId>/v1_Contract.pdf
|
|
106
|
+
│ ├── Attachment/<ParentId>/<Id>_logo.png
|
|
107
|
+
│ ├── Document/<FolderId>/<Id>_Letterhead.docx
|
|
108
|
+
│ └── index.csv ← file → record mapping, SHA-256, MD5 checked against Salesforce
|
|
109
|
+
├── schema/Account.json ← full describe: labels, picklist values, relationships
|
|
110
|
+
├── schema.postgres.sql ← ready-to-run DDL (also .duckdb.sql / .sqlite.sql)
|
|
111
|
+
├── erd.mmd ← Mermaid diagram of your data model
|
|
112
|
+
├── manifest.json ← counts + checksums, machine-readable
|
|
113
|
+
└── REPORT.md ← human summary: what was exported, what was skipped and why
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Then query it however you like:
|
|
117
|
+
|
|
118
|
+
```sql
|
|
119
|
+
-- duckdb salesforce.duckdb
|
|
120
|
+
SELECT a.Name, count(*) AS open_opps, sum(o.Amount) AS pipeline
|
|
121
|
+
FROM Opportunity o JOIN Account a ON a.Id = o.AccountId
|
|
122
|
+
WHERE NOT o.IsClosed
|
|
123
|
+
GROUP BY 1 ORDER BY pipeline DESC LIMIT 10;
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Why sf-exit
|
|
127
|
+
|
|
128
|
+
Your CRM data belongs to you, but getting *all* of it out of Salesforce in a usable form is surprisingly hard.
|
|
129
|
+
|
|
130
|
+
| | **sf-exit** | Data Export Service | Data Loader | Paid backup tools |
|
|
131
|
+
|---------------------------------------------|:---:|:---:|:---:|:---:|
|
|
132
|
+
| Every object in one run | ✅ | ✅ | ❌ one at a time | ✅ |
|
|
133
|
+
| Files, attachments & documents, linked to records | ✅ | ✅ zipped | ❌ | ✅ |
|
|
134
|
+
| Run whenever you want | ✅ | ❌ weekly or monthly | ✅ | ✅ |
|
|
135
|
+
| Resumes after interruption | ✅ | ❌ | ❌ | varies |
|
|
136
|
+
| Typed SQL schema + relationships | ✅ | ❌ | ❌ | rarely |
|
|
137
|
+
| Loads into Postgres / DuckDB / SQLite / Parquet | ✅ | ❌ | ❌ | rarely |
|
|
138
|
+
| Checksums + verify against the live org | ✅ | ❌ | ❌ | varies |
|
|
139
|
+
| Scriptable / CI-friendly | ✅ | ❌ | partly | API only |
|
|
140
|
+
| Data never leaves your machine | ✅ | ✅ | ✅ | ❌ stored by vendor |
|
|
141
|
+
| Price | **free, MIT** | included | free | $$ per user / month |
|
|
142
|
+
|
|
143
|
+
Use it to **migrate** to another CRM, **archive** an org before you shut it down, take **independent backups**, or feed a **data warehouse**.
|
|
144
|
+
|
|
145
|
+
## Features
|
|
146
|
+
|
|
147
|
+
- **Complete by default.** It finds every queryable object (standard, custom, history, activities, Chatter, email, custom metadata) and every field. It also explains anything it skips in `REPORT.md`.
|
|
148
|
+
- **Fast on big orgs and light on API limits.** Large objects go through **Bulk API 2.0**, several at a time. Small objects are described and fetched **25 per API call** using composite requests, so a typical org's hundreds of small objects cost dozens of calls, not a thousand. If Bulk refuses an object, it falls back to REST.
|
|
149
|
+
- **Handles Salesforce's quirks.** Knowledge articles (`__kav`, which need a status filter) and `ContentDocumentLink` (which needs an Id filter) are exported anyway. System objects that can't be exported are skipped with the reason, instead of failing your run.
|
|
150
|
+
- **Files that stay connected.** ContentVersion, Attachment and Document bodies are downloaded in parallel, checked against Salesforce's own MD5 checksum, and mapped back to their records via `ContentDocumentLink` and `files/index.csv`.
|
|
151
|
+
- **Resumable.** Press Ctrl-C, lose Wi-Fi or hit an API limit, then run the same command again. Finished objects and files are skipped.
|
|
152
|
+
- **Deleted and archived records** with `--include-deleted` (Recycle Bin, archived activities).
|
|
153
|
+
- **Real schemas.** Typed DDL for Postgres, DuckDB and SQLite, plus a Mermaid ER diagram. `sf-exit load` creates typed tables: dates are dates, booleans are booleans, currency is numeric.
|
|
154
|
+
- **Verifiable.** `sf-exit verify` re-counts every CSV and re-hashes every file. `--live` compares counts against the org.
|
|
155
|
+
- **Read-only and private.** sf-exit never modifies your org and has no telemetry. Data goes straight from Salesforce to your disk.
|
|
156
|
+
- **Plays well with others.** It's a plain folder of CSVs. Also available as a pip package, a Docker image and a GitHub Action.
|
|
157
|
+
|
|
158
|
+
## Usage
|
|
159
|
+
|
|
160
|
+
### Authentication
|
|
161
|
+
|
|
162
|
+
| Method | Command | Best for |
|
|
163
|
+
|---|---|---|
|
|
164
|
+
| Salesforce CLI | `sf-exit export --org my-alias` | Laptops. Zero setup. If you omit `--org`, your default CLI org is used |
|
|
165
|
+
| Access token | `--instance-url https://acme.my.salesforce.com --access-token 00D…` | One-offs, other tools |
|
|
166
|
+
| OAuth client credentials | `--instance-url … --client-id … --client-secret …` | Servers, CI ([setup](docs/authentication.md#client-credentials)) |
|
|
167
|
+
| JWT bearer | `--client-id … --username … --jwt-key-file server.key` | CI with a certificate (`pip install 'sf-exit[jwt]'`) |
|
|
168
|
+
| Username + password | `--client-id … --client-secret … -u … --password 'pw+token'` | Legacy setups |
|
|
169
|
+
|
|
170
|
+
Every flag can also be set as an environment variable (`SF_ORG`, `SF_INSTANCE_URL`, `SF_ACCESS_TOKEN`, `SF_CLIENT_ID`, `SF_CLIENT_SECRET`, `SF_USERNAME`, `SF_PASSWORD`, `SF_JWT_KEY_FILE`). Add `--sandbox` for test.salesforce.com logins. More detail is in [docs/authentication.md](docs/authentication.md).
|
|
171
|
+
|
|
172
|
+
> **Permissions:** for a complete export the user should have **View All Data** and **Query All Files**. Without them, Salesforce silently returns only the records and files that user can see. `sf-exit doctor` checks this for you.
|
|
173
|
+
|
|
174
|
+
### Choosing what to export
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
sf-exit export -i Account -i Contact -i 'Invoice*__c' # only these (globs, case-insensitive)
|
|
178
|
+
sf-exit export -x '*History' -x '*Share' -x '*Feed' # everything except these
|
|
179
|
+
sf-exit export --include-deleted # + Recycle Bin and archived records
|
|
180
|
+
sf-exit export --no-files # records only
|
|
181
|
+
sf-exit export --files-only --all-file-versions # every version of every file
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Loading into a database
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
sf-exit load ./export --to sqlite:///salesforce.db # built in
|
|
188
|
+
sf-exit load ./export --to duckdb:///salesforce.duckdb # pip install 'sf-exit[duckdb]'
|
|
189
|
+
sf-exit load ./export --to parquet:///./parquet # pip install 'sf-exit[parquet]'
|
|
190
|
+
sf-exit load ./export --to postgresql://me:pw@localhost/crm --lowercase # pip install 'sf-exit[postgres]'
|
|
191
|
+
|
|
192
|
+
sf-exit export --org my-org --load duckdb:///salesforce.duckdb # export + load in one go
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Use `pip install 'sf-exit[all]'` to get every extra.
|
|
196
|
+
|
|
197
|
+
### Verifying an export
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
sf-exit verify ./export # offline: CSV row counts + SHA-256 of every data file and binary file
|
|
201
|
+
sf-exit verify ./export --live # also compare row counts against the org right now
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Docker
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
docker run --rm -it -v "$PWD:/work" -e SF_INSTANCE_URL -e SF_ACCESS_TOKEN \
|
|
208
|
+
ghcr.io/akhsterminator/sf-exit export -d /work/export
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Nightly backups with GitHub Actions
|
|
212
|
+
|
|
213
|
+
sf-exit ships as a GitHub Action. Put this in a **private** repository and you have free, versioned, off-platform Salesforce backups:
|
|
214
|
+
|
|
215
|
+
```yaml
|
|
216
|
+
# .github/workflows/salesforce-backup.yml
|
|
217
|
+
on:
|
|
218
|
+
schedule: [{ cron: "0 2 * * *" }]
|
|
219
|
+
workflow_dispatch:
|
|
220
|
+
jobs:
|
|
221
|
+
backup:
|
|
222
|
+
runs-on: ubuntu-latest
|
|
223
|
+
steps:
|
|
224
|
+
- uses: akhsterminator/sf-exit@v0
|
|
225
|
+
with:
|
|
226
|
+
args: export -d sf-export --load parquet:///sf-parquet
|
|
227
|
+
env:
|
|
228
|
+
SF_INSTANCE_URL: ${{ secrets.SF_INSTANCE_URL }}
|
|
229
|
+
SF_CLIENT_ID: ${{ secrets.SF_CLIENT_ID }}
|
|
230
|
+
SF_CLIENT_SECRET: ${{ secrets.SF_CLIENT_SECRET }}
|
|
231
|
+
- uses: actions/upload-artifact@v7
|
|
232
|
+
with: { name: salesforce-backup, path: sf-export, retention-days: 30 }
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
There's a fuller example, including upload to S3, in [examples/github-actions-backup.yml](examples/github-actions-backup.yml).
|
|
236
|
+
|
|
237
|
+
## How it works
|
|
238
|
+
|
|
239
|
+
```mermaid
|
|
240
|
+
flowchart LR
|
|
241
|
+
A[describeGlobal] --> B[describe each object<br/>skip compound & blob fields]
|
|
242
|
+
B --> C{rows ≥ 10k?}
|
|
243
|
+
C -- yes --> D[Bulk API 2.0 query job<br/>stream CSV pages]
|
|
244
|
+
C -- no --> E[REST query<br/>paginate JSON → CSV]
|
|
245
|
+
D -. refused .-> E
|
|
246
|
+
D & E --> F[data/*.csv + state.json]
|
|
247
|
+
F --> G[ContentDocumentLink<br/>chunked by document Id]
|
|
248
|
+
G --> H[download file bodies<br/>in parallel, MD5-verified]
|
|
249
|
+
H --> I[schema.sql · erd.mmd · manifest.json · REPORT.md]
|
|
250
|
+
I --> J[(sf-exit load →<br/>SQLite / DuckDB / Postgres / Parquet)]
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## FAQ
|
|
254
|
+
|
|
255
|
+
<details><summary><b>Will this use up my API limits?</b></summary>
|
|
256
|
+
|
|
257
|
+
Usually not by much. A Bulk API job costs a handful of calls whatever its size. Small objects are fetched 25 per call, and each file costs one call. `sf-exit plan` estimates the total and shows your remaining daily allowance before you start. Use `--workers 1` to go gently on a busy org.
|
|
258
|
+
</details>
|
|
259
|
+
|
|
260
|
+
<details><summary><b>How long does it take?</b></summary>
|
|
261
|
+
|
|
262
|
+
It depends mostly on Salesforce's Bulk API and your file storage. As a rough guide, millions of records take minutes to tens of minutes, and file downloads are limited by bandwidth. Because exports resume, a long run can be spread over several sessions.
|
|
263
|
+
</details>
|
|
264
|
+
|
|
265
|
+
<details><summary><b>Does it export metadata (Apex, flows, layouts, reports)?</b></summary>
|
|
266
|
+
|
|
267
|
+
Not yet. sf-exit focuses on **data**, but it does save each object's schema (fields, picklists, relationships) as JSON and SQL. For metadata, use `sf project retrieve start --manifest package.xml` today. Native support is on the [roadmap](#roadmap).
|
|
268
|
+
</details>
|
|
269
|
+
|
|
270
|
+
<details><summary><b>Which editions are supported?</b></summary>
|
|
271
|
+
|
|
272
|
+
Any org with API access: Enterprise, Unlimited, Performance and Developer editions, plus Professional Edition with the API add-on. Sandboxes work with `--sandbox` or a sandbox CLI alias.
|
|
273
|
+
</details>
|
|
274
|
+
|
|
275
|
+
<details><summary><b>Why are some objects skipped?</b></summary>
|
|
276
|
+
|
|
277
|
+
Some "objects" are really metadata views (`FieldDefinition`), virtual roll-ups of data exported elsewhere (`OpenActivity`, `CombinedAttachment`), streaming events (`*ChangeEvent`), external objects (`__x`) or big objects (`__b`). Every skip and its reason is listed in `REPORT.md`. Run with `-v` to see them in `sf-exit plan`.
|
|
278
|
+
</details>
|
|
279
|
+
|
|
280
|
+
<details><summary><b>What about compound fields like addresses?</b></summary>
|
|
281
|
+
|
|
282
|
+
The Bulk API can't return compound fields, so sf-exit exports their components instead: `BillingStreet`, `BillingCity`, `BillingPostalCode` and so on. No data is lost.
|
|
283
|
+
</details>
|
|
284
|
+
|
|
285
|
+
<details><summary><b>Is my data sent anywhere?</b></summary>
|
|
286
|
+
|
|
287
|
+
No. sf-exit talks only to your Salesforce instance and writes only to your disk. There's no telemetry, no account and no server.
|
|
288
|
+
</details>
|
|
289
|
+
|
|
290
|
+
<details><summary><b>Can I put the data back into Salesforce, or into another CRM?</b></summary>
|
|
291
|
+
|
|
292
|
+
The export keeps every original Id and relationship column, which is exactly what you need to rebuild links in a target system. Guided re-import into another org, HubSpot or Attio is on the roadmap, and recipes are very welcome as PRs.
|
|
293
|
+
</details>
|
|
294
|
+
|
|
295
|
+
## Roadmap
|
|
296
|
+
|
|
297
|
+
- [ ] Incremental exports (only changed rows, via `SystemModstamp`)
|
|
298
|
+
- [ ] Metadata export (Apex, flows, layouts, reports) via the Metadata API
|
|
299
|
+
- [ ] Write straight to S3 / GCS / Azure Blob
|
|
300
|
+
- [ ] Import helpers: Salesforce → another Salesforce org, HubSpot, Attio, Twenty
|
|
301
|
+
- [ ] Field-level PII redaction for sharing exports safely
|
|
302
|
+
- [ ] Big object (`__b`) export
|
|
303
|
+
- [ ] Single-file binaries (Homebrew, Scoop)
|
|
304
|
+
|
|
305
|
+
Have an idea or need? [Open an issue](https://github.com/akhsterminator/sf-exit/issues/new/choose). Good first issues are [labelled](https://github.com/akhsterminator/sf-exit/labels/good%20first%20issue).
|
|
306
|
+
|
|
307
|
+
## Contributing
|
|
308
|
+
|
|
309
|
+
Contributions are welcome! The whole test suite runs against an in-process **fake Salesforce org** (`tests/fake_org.py`), so you don't need a Salesforce account to hack on sf-exit:
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
git clone https://github.com/akhsterminator/sf-exit && cd sf-exit
|
|
313
|
+
pip install -e '.[dev]'
|
|
314
|
+
pytest
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
|
318
|
+
|
|
319
|
+
## Star history
|
|
320
|
+
|
|
321
|
+
If sf-exit saved you a migration project or a backup bill, a ⭐ helps other people find it.
|
|
322
|
+
|
|
323
|
+
[](https://star-history.com/#akhsterminator/sf-exit&Date)
|
|
324
|
+
|
|
325
|
+
## License
|
|
326
|
+
|
|
327
|
+
[MIT](LICENSE). sf-exit is an independent open-source project and is not affiliated with, endorsed by or sponsored by Salesforce, Inc. Salesforce is a trademark of Salesforce, Inc.
|