@fazetitans/fscopy 1.0.0

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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +409 -0
  3. package/package.json +74 -0
  4. package/src/cli.ts +1936 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 [fullname]
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.
package/README.md ADDED
@@ -0,0 +1,409 @@
1
+ <p align="center">
2
+ <img src="assets/banner.png" alt="fscopy banner" width="600">
3
+ </p>
4
+
5
+ <h1 align="center">
6
+ <img src="assets/logo.png" alt="fscopy logo" width="40" height="40" style="vertical-align: middle;">
7
+ fscopy
8
+ </h1>
9
+
10
+ <p align="center">
11
+ <a href="https://www.npmjs.com/package/@fazetitans/fscopy"><img src="https://img.shields.io/npm/v/@fazetitans/fscopy.svg" alt="npm version"></a>
12
+ <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
13
+ </p>
14
+
15
+ <p align="center">
16
+ <strong>Fast CLI tool to copy Firestore collections between Firebase projects</strong>
17
+ </p>
18
+
19
+ Transfer documents between Firebase projects with support for subcollections, filtering, parallel transfers, and merge mode. Built with [Bun](https://bun.sh) for maximum performance.
20
+
21
+ ## Features
22
+
23
+ - **Subcollection support** - Recursively copy nested collections
24
+ - **Document filtering** - Filter documents with `--where` clauses
25
+ - **Exclude patterns** - Skip subcollections by name or glob pattern
26
+ - **Merge mode** - Update existing documents instead of overwriting
27
+ - **Parallel transfers** - Copy multiple collections simultaneously
28
+ - **Clear destination** - Optionally delete destination data before transfer
29
+ - **Sync mode** - Delete destination docs not present in source
30
+ - **Document transform** - Transform data during transfer with custom JS/TS functions
31
+ - **Collection renaming** - Rename collections in destination for backups or migrations
32
+ - **ID modification** - Add prefix or suffix to document IDs to avoid conflicts
33
+ - **Webhook notifications** - Send Slack, Discord, or custom webhooks on completion
34
+ - **Resume transfers** - Continue interrupted transfers from saved state
35
+ - **Interactive mode** - Guided setup with prompts for project and collection selection
36
+ - **Progress bar** - Real-time progress with ETA
37
+ - **Automatic retry** - Exponential backoff on network errors
38
+ - **Dry run mode** - Preview changes before applying (enabled by default)
39
+ - **Flexible config** - INI, JSON, or CLI arguments
40
+
41
+ ## Installation
42
+
43
+ ### With Bun (recommended)
44
+
45
+ ```bash
46
+ # Global install
47
+ bun add -g @fazetitans/fscopy
48
+
49
+ # Or run directly
50
+ bunx @fazetitans/fscopy --help
51
+ ```
52
+
53
+ ### With npm
54
+
55
+ ```bash
56
+ npm install -g @fazetitans/fscopy
57
+ ```
58
+
59
+ ### Prerequisites
60
+
61
+ 1. [Bun](https://bun.sh) or Node.js 18+
62
+ 1. Google Cloud authentication: `gcloud auth application-default login`
63
+ 1. Firestore read access on source project, write access on destination
64
+
65
+ ## Quick Start
66
+
67
+ ```bash
68
+ # 1. Generate config file
69
+ fscopy --init config.ini
70
+
71
+ # 2. Edit config.ini with your project IDs and collections
72
+
73
+ # 3. Preview transfer (dry run)
74
+ fscopy -f config.ini
75
+
76
+ # 4. Execute transfer
77
+ fscopy -f config.ini -d false -y
78
+ ```
79
+
80
+ ## Usage
81
+
82
+ ### Basic Transfer
83
+
84
+ ```bash
85
+ # Using config file
86
+ fscopy -f config.ini
87
+
88
+ # Using CLI arguments
89
+ fscopy \
90
+ --source-project my-source \
91
+ --dest-project my-dest \
92
+ -c users orders products
93
+ ```
94
+
95
+ ### With Subcollections
96
+
97
+ ```bash
98
+ # Include all subcollections
99
+ fscopy -f config.ini -s
100
+
101
+ # Exclude specific subcollections
102
+ fscopy -f config.ini -s --exclude logs --exclude "temp/*"
103
+ ```
104
+
105
+ ### Filtering Documents
106
+
107
+ ```bash
108
+ # Single filter
109
+ fscopy -f config.ini --where "status == active"
110
+
111
+ # Multiple filters (AND)
112
+ fscopy -f config.ini -w "active == true" -w "createdAt > 2024-01-01"
113
+
114
+ # Supported operators: ==, !=, <, >, <=, >=
115
+ ```
116
+
117
+ ### Advanced Options
118
+
119
+ ```bash
120
+ # Merge mode (update instead of overwrite)
121
+ fscopy -f config.ini --merge
122
+
123
+ # Parallel transfers (3 collections at once)
124
+ fscopy -f config.ini --parallel 3
125
+
126
+ # With logging
127
+ fscopy -f config.ini --log transfer.log
128
+
129
+ # Limit documents per collection
130
+ fscopy -f config.ini --limit 100
131
+
132
+ # Quiet mode (no progress bar)
133
+ fscopy -f config.ini -q
134
+
135
+ # Clear destination before transfer (DESTRUCTIVE)
136
+ fscopy -f config.ini --clear
137
+
138
+ # Sync mode: delete orphan docs in destination
139
+ fscopy -f config.ini --delete-missing
140
+
141
+ # Interactive mode with prompts
142
+ fscopy -i
143
+
144
+ # Transform documents during transfer
145
+ fscopy -f config.ini --transform ./transforms/anonymize.ts
146
+
147
+ # Rename collections in destination
148
+ fscopy -f config.ini -r users:users_backup -r orders:orders_2024
149
+
150
+ # Add prefix to document IDs
151
+ fscopy -f config.ini --id-prefix backup_
152
+
153
+ # Add suffix to document IDs
154
+ fscopy -f config.ini --id-suffix _archived
155
+
156
+ # Send notification to Slack/Discord
157
+ fscopy -f config.ini --webhook https://hooks.slack.com/services/...
158
+
159
+ # Resume an interrupted transfer
160
+ fscopy -f config.ini --resume
161
+ ```
162
+
163
+ ### Collection Renaming
164
+
165
+ Rename collections during transfer for backups or migrations:
166
+
167
+ ```bash
168
+ # Backup with dated collection names
169
+ fscopy -f config.ini -r users:users_2024_12_29 -r orders:orders_2024_12_29
170
+
171
+ # Multiple renames in one command
172
+ fscopy -f config.ini --rename-collection users:users_v2 --rename-collection products:catalog
173
+ ```
174
+
175
+ Subcollections are automatically renamed along with their parent collection.
176
+
177
+ ### ID Modification
178
+
179
+ Add prefix or suffix to document IDs to avoid conflicts when merging:
180
+
181
+ ```bash
182
+ # Add prefix: user123 → backup_user123
183
+ fscopy -f config.ini --id-prefix backup_
184
+
185
+ # Add suffix: user123 → user123_v2
186
+ fscopy -f config.ini --id-suffix _v2
187
+
188
+ # Combine both: user123 → old_user123_archived
189
+ fscopy -f config.ini --id-prefix old_ --id-suffix _archived
190
+ ```
191
+
192
+ ### Document Transform
193
+
194
+ Transform documents during transfer using a custom function:
195
+
196
+ ```bash
197
+ # Create a transform file
198
+ cat > anonymize.ts << 'EOF'
199
+ export function transform(doc, meta) {
200
+ // Anonymize email addresses
201
+ if (doc.email) {
202
+ doc.email = `user_${meta.id}@example.com`;
203
+ }
204
+ // Remove sensitive fields
205
+ delete doc.password;
206
+ delete doc.ssn;
207
+ // Return null to skip the document
208
+ if (doc.deleted) return null;
209
+ return doc;
210
+ }
211
+ EOF
212
+
213
+ # Use the transform
214
+ fscopy -f config.ini -t ./anonymize.ts
215
+ ```
216
+
217
+ The transform function receives:
218
+
219
+ - `doc` - The document data as an object
220
+ - `meta` - Metadata with `id` (document ID) and `path` (full document path)
221
+
222
+ Return the transformed document, or `null` to skip it.
223
+
224
+ ### Webhook Notifications
225
+
226
+ Get notified when transfers complete (success or failure):
227
+
228
+ ```bash
229
+ # Slack webhook
230
+ fscopy -f config.ini --webhook https://hooks.slack.com/services/XXX/YYY/ZZZ
231
+
232
+ # Discord webhook
233
+ fscopy -f config.ini --webhook https://discord.com/api/webhooks/123/abc
234
+
235
+ # Custom webhook (receives raw JSON payload)
236
+ fscopy -f config.ini --webhook https://api.example.com/webhook
237
+ ```
238
+
239
+ The webhook receives a POST request with:
240
+
241
+ - `source` / `destination` - Project IDs
242
+ - `collections` - List of transferred collections
243
+ - `stats` - Documents transferred, deleted, errors
244
+ - `duration` - Transfer time in seconds
245
+ - `dryRun` - Whether it was a dry run
246
+ - `success` - Boolean status
247
+ - `error` - Error message (if failed)
248
+
249
+ Slack and Discord webhooks are automatically formatted with rich messages.
250
+
251
+ ### Resume Interrupted Transfers
252
+
253
+ Large migrations can be resumed if interrupted:
254
+
255
+ ```bash
256
+ # Start a transfer (state is saved automatically to .fscopy-state.json)
257
+ fscopy -f config.ini -d false
258
+
259
+ # If interrupted (Ctrl+C, network error, etc.), resume from where it left off
260
+ fscopy -f config.ini --resume
261
+
262
+ # Use a custom state file
263
+ fscopy -f config.ini --state-file ./my-transfer.state.json
264
+ fscopy -f config.ini --resume --state-file ./my-transfer.state.json
265
+ ```
266
+
267
+ The state file tracks:
268
+
269
+ - Completed document IDs per collection
270
+ - Transfer statistics
271
+ - Source/destination project validation
272
+
273
+ State files are automatically deleted on successful completion.
274
+
275
+ ## Configuration
276
+
277
+ ### INI Format (recommended)
278
+
279
+ ```bash
280
+ fscopy --init config.ini
281
+ ```
282
+
283
+ ```ini
284
+ [projects]
285
+ source = my-source-project
286
+ dest = my-dest-project
287
+
288
+ [transfer]
289
+ collections = users, orders, products
290
+ includeSubcollections = true
291
+ dryRun = true
292
+ batchSize = 500
293
+ limit = 0
294
+
295
+ [options]
296
+ ; where = status == active
297
+ ; exclude = logs, cache, temp/*
298
+ merge = false
299
+ parallel = 1
300
+ clear = false
301
+ deleteMissing = false
302
+ ; transform = ./transforms/anonymize.ts
303
+ ; renameCollection = users:users_backup, orders:orders_2024
304
+ ; idPrefix = backup_
305
+ ; idSuffix = _v2
306
+ ```
307
+
308
+ ### JSON Format
309
+
310
+ ```bash
311
+ fscopy --init config.json
312
+ ```
313
+
314
+ ```json
315
+ {
316
+ "sourceProject": "my-source-project",
317
+ "destProject": "my-dest-project",
318
+ "collections": ["users", "orders"],
319
+ "includeSubcollections": true,
320
+ "dryRun": true,
321
+ "batchSize": 500,
322
+ "limit": 0,
323
+ "where": ["status == active"],
324
+ "exclude": ["logs", "cache"],
325
+ "merge": false,
326
+ "parallel": 1,
327
+ "clear": false,
328
+ "deleteMissing": false,
329
+ "transform": null,
330
+ "renameCollection": {},
331
+ "idPrefix": null,
332
+ "idSuffix": null
333
+ }
334
+ ```
335
+
336
+ ## CLI Reference
337
+
338
+ | Option | Alias | Type | Default | Description |
339
+ | -------------------------- | ----- | ------- | ------- | --------------------------------- |
340
+ | `--init` | | string | | Generate config template |
341
+ | `--config` | `-f` | string | | Path to config file |
342
+ | `--source-project` | | string | | Source Firebase project |
343
+ | `--dest-project` | | string | | Destination project |
344
+ | `--collections` | `-c` | array | | Collections to transfer |
345
+ | `--include-subcollections` | `-s` | boolean | `false` | Include subcollections |
346
+ | `--where` | `-w` | array | | Filter documents |
347
+ | `--exclude` | `-x` | array | | Exclude subcollections |
348
+ | `--merge` | `-m` | boolean | `false` | Merge instead of overwrite |
349
+ | `--parallel` | `-p` | number | `1` | Parallel transfers |
350
+ | `--dry-run` | `-d` | boolean | `true` | Preview without writing |
351
+ | `--batch-size` | `-b` | number | `500` | Documents per batch |
352
+ | `--limit` | `-l` | number | `0` | Limit docs (0 = no limit) |
353
+ | `--retries` | | number | `3` | Retries on error |
354
+ | `--log` | | string | | Log file path |
355
+ | `--quiet` | `-q` | boolean | `false` | No progress bar |
356
+ | `--yes` | `-y` | boolean | `false` | Skip confirmation |
357
+ | `--clear` | | boolean | `false` | Clear destination before transfer |
358
+ | `--delete-missing` | | boolean | `false` | Delete dest docs not in source |
359
+ | `--interactive` | `-i` | boolean | `false` | Interactive mode with prompts |
360
+ | `--transform` | `-t` | string | | Path to JS/TS transform file |
361
+ | `--rename-collection` | `-r` | array | | Rename collection (source:dest) |
362
+ | `--id-prefix` | | string | | Add prefix to document IDs |
363
+ | `--id-suffix` | | string | | Add suffix to document IDs |
364
+ | `--webhook` | | string | | Webhook URL for notifications |
365
+ | `--resume` | | boolean | `false` | Resume from saved state |
366
+ | `--state-file` | | string | `.fscopy-state.json` | State file path |
367
+
368
+ ## How It Works
369
+
370
+ 1. **Authentication** - Uses Google Application Default Credentials (ADC)
371
+ 2. **Document counting** - Counts total documents for progress bar
372
+ 3. **Batch processing** - Transfers documents in configurable batches
373
+ 4. **Retry logic** - Automatic retry with exponential backoff on failures
374
+ 5. **Subcollection discovery** - Uses `listCollections()` to find nested data
375
+
376
+ ## Notes
377
+
378
+ - **Dry run is ON by default** - Use `-d false` for actual transfer
379
+ - **Documents are overwritten** - Use `--merge` to update instead
380
+ - **Where filters apply to root only** - Subcollections are copied in full
381
+ - **Exclude patterns support globs** - e.g., `temp/*`, `*/logs`
382
+ - **Progress bar shows ETA** - Based on documents processed
383
+ - **Clear is destructive** - `--clear` deletes all destination docs before transfer
384
+ - **Delete-missing syncs** - `--delete-missing` removes orphan docs after transfer
385
+ - **Transform applies to all** - Transform function is applied to both root and subcollection docs
386
+ - **Same project allowed** - Source and destination can be the same project when using `--rename-collection` or `--id-prefix`/`--id-suffix`
387
+
388
+ ## Development
389
+
390
+ ```bash
391
+ # Clone and install
392
+ git clone https://github.com/fazetitans/fscopy.git
393
+ cd fscopy
394
+ bun install
395
+
396
+ # Run locally
397
+ bun start -- -f config.ini
398
+
399
+ # Run tests
400
+ bun test
401
+
402
+ # Type check & lint
403
+ bun run type-check
404
+ bun run lint
405
+ ```
406
+
407
+ ## License
408
+
409
+ [MIT](LICENSE)
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "@fazetitans/fscopy",
3
+ "version": "1.0.0",
4
+ "description": "Fast CLI tool to copy Firestore collections between Firebase projects with filtering, parallel transfers, and subcollection support",
5
+ "type": "module",
6
+ "bin": {
7
+ "fscopy": "./src/cli.ts"
8
+ },
9
+ "scripts": {
10
+ "start": "bun src/cli.ts",
11
+ "dev": "bun --watch src/cli.ts",
12
+ "test": "bun test",
13
+ "test:watch": "bun test --watch",
14
+ "type-check": "tsc --noEmit",
15
+ "lint": "eslint src/**/*.ts",
16
+ "lint:fix": "eslint src/**/*.ts --fix",
17
+ "format": "prettier --write src/**/*.ts",
18
+ "format:check": "prettier --check src/**/*.ts",
19
+ "prepublishOnly": "bun run type-check && bun run lint && bun test"
20
+ },
21
+ "keywords": [
22
+ "firestore",
23
+ "firebase",
24
+ "transfer",
25
+ "migration",
26
+ "cli",
27
+ "copy",
28
+ "backup",
29
+ "sync",
30
+ "google-cloud",
31
+ "database",
32
+ "nosql",
33
+ "bun"
34
+ ],
35
+ "author": "fazetitans",
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/fazetitans/fscopy.git"
40
+ },
41
+ "homepage": "https://github.com/fazetitans/fscopy#readme",
42
+ "bugs": {
43
+ "url": "https://github.com/fazetitans/fscopy/issues"
44
+ },
45
+ "engines": {
46
+ "node": ">=18.0.0"
47
+ },
48
+ "files": [
49
+ "src/cli.ts",
50
+ "README.md",
51
+ "LICENSE"
52
+ ],
53
+ "dependencies": {
54
+ "@inquirer/prompts": "^8.1.0",
55
+ "cli-progress": "^3.12.0",
56
+ "firebase-admin": "^13.6.0",
57
+ "ini": "^6.0.0",
58
+ "yargs": "^17.7.2"
59
+ },
60
+ "devDependencies": {
61
+ "@eslint/js": "^9.39.2",
62
+ "@types/cli-progress": "^3.11.6",
63
+ "@types/ini": "^4.1.1",
64
+ "@types/node": "^25.0.3",
65
+ "@types/yargs": "^17.0.35",
66
+ "@typescript-eslint/eslint-plugin": "^8.51.0",
67
+ "@typescript-eslint/parser": "^8.51.0",
68
+ "bun-types": "^1.3.5",
69
+ "eslint": "^9.39.2",
70
+ "prettier": "^3.7.4",
71
+ "typescript": "^5.9.3",
72
+ "typescript-eslint": "^8.51.0"
73
+ }
74
+ }