@x12i/catalox 2.3.0 → 2.4.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.
- package/README.md +27 -6
- package/dist/src/catalox/backup-data.d.ts +40 -0
- package/dist/src/catalox/backup-data.d.ts.map +1 -0
- package/dist/src/catalox/backup-data.js +522 -0
- package/dist/src/catalox/backup-data.js.map +1 -0
- package/dist/src/catalox/catalox.d.ts +25 -0
- package/dist/src/catalox/catalox.d.ts.map +1 -1
- package/dist/src/catalox/catalox.js +63 -1
- package/dist/src/catalox/catalox.js.map +1 -1
- package/dist/src/catalox/index.d.ts +3 -0
- package/dist/src/catalox/index.d.ts.map +1 -1
- package/dist/src/catalox/index.js +3 -0
- package/dist/src/catalox/index.js.map +1 -1
- package/dist/src/catalox/native-catalog-layout-diagnostics.d.ts +29 -0
- package/dist/src/catalox/native-catalog-layout-diagnostics.d.ts.map +1 -0
- package/dist/src/catalox/native-catalog-layout-diagnostics.js +55 -0
- package/dist/src/catalox/native-catalog-layout-diagnostics.js.map +1 -0
- package/dist/src/catalox/restore-firestore-backup.d.ts +13 -0
- package/dist/src/catalox/restore-firestore-backup.d.ts.map +1 -0
- package/dist/src/catalox/restore-firestore-backup.js +326 -0
- package/dist/src/catalox/restore-firestore-backup.js.map +1 -0
- package/dist/src/cli/index.js +143 -0
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/contracts/backup.d.ts +36 -0
- package/dist/src/contracts/backup.d.ts.map +1 -0
- package/dist/src/contracts/backup.js +2 -0
- package/dist/src/contracts/backup.js.map +1 -0
- package/dist/src/contracts/catalog-data-index.d.ts +13 -0
- package/dist/src/contracts/catalog-data-index.d.ts.map +1 -0
- package/dist/src/contracts/catalog-data-index.js +2 -0
- package/dist/src/contracts/catalog-data-index.js.map +1 -0
- package/dist/src/contracts/catalogs.d.ts +2 -1
- package/dist/src/contracts/catalogs.d.ts.map +1 -1
- package/dist/src/contracts/index.d.ts +3 -0
- package/dist/src/contracts/index.d.ts.map +1 -1
- package/dist/src/contracts/index.js.map +1 -1
- package/dist/src/contracts/restore.d.ts +44 -0
- package/dist/src/contracts/restore.d.ts.map +1 -0
- package/dist/src/contracts/restore.js +2 -0
- package/dist/src/contracts/restore.js.map +1 -0
- package/dist/src/firebase/catalog-data-index-store.d.ts +14 -0
- package/dist/src/firebase/catalog-data-index-store.d.ts.map +1 -0
- package/dist/src/firebase/catalog-data-index-store.js +33 -0
- package/dist/src/firebase/catalog-data-index-store.js.map +1 -0
- package/dist/src/firebase/catalog-data-paths.d.ts +14 -0
- package/dist/src/firebase/catalog-data-paths.d.ts.map +1 -0
- package/dist/src/firebase/catalog-data-paths.js +31 -0
- package/dist/src/firebase/catalog-data-paths.js.map +1 -0
- package/dist/src/firebase/index.d.ts +2 -0
- package/dist/src/firebase/index.d.ts.map +1 -1
- package/dist/src/firebase/index.js +2 -0
- package/dist/src/firebase/index.js.map +1 -1
- package/dist/src/firebase/native-item-store.d.ts +5 -1
- package/dist/src/firebase/native-item-store.d.ts.map +1 -1
- package/dist/src/firebase/native-item-store.js +33 -15
- package/dist/src/firebase/native-item-store.js.map +1 -1
- package/dist/src/migrations/index.d.ts +1 -0
- package/dist/src/migrations/index.d.ts.map +1 -1
- package/dist/src/migrations/index.js +1 -0
- package/dist/src/migrations/index.js.map +1 -1
- package/dist/src/migrations/migrate-native-catalog-layout.d.ts +51 -0
- package/dist/src/migrations/migrate-native-catalog-layout.d.ts.map +1 -0
- package/dist/src/migrations/migrate-native-catalog-layout.js +224 -0
- package/dist/src/migrations/migrate-native-catalog-layout.js.map +1 -0
- package/dist/test/integration/firestore.emulator.test.js +10 -2
- package/dist/test/integration/firestore.emulator.test.js.map +1 -1
- package/dist/test/unit/backup-timestamp.test.d.ts +2 -0
- package/dist/test/unit/backup-timestamp.test.d.ts.map +1 -0
- package/dist/test/unit/backup-timestamp.test.js +11 -0
- package/dist/test/unit/backup-timestamp.test.js.map +1 -0
- package/dist/test/unit/resolve-native-items-layout.test.d.ts +2 -0
- package/dist/test/unit/resolve-native-items-layout.test.d.ts.map +1 -0
- package/dist/test/unit/resolve-native-items-layout.test.js +50 -0
- package/dist/test/unit/resolve-native-items-layout.test.js.map +1 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -82,13 +82,23 @@ The `catalox` binary loads `.env` via `dotenv`. Use **`CATALOX_*`** for **Catalo
|
|
|
82
82
|
- **`CATALOX_USER_ID`** — Optional user id / actor for authz-sensitive CLI paths.
|
|
83
83
|
- **`CATALOX_MONGO_URI`** — If set, enables the Mongo catalog adapter in the CLI (see `createCatalox()` in `src/cli/index.ts`).
|
|
84
84
|
|
|
85
|
+
**Running commands from this repository:** the shell command `catalox` is only on your `PATH` if the package is installed globally (`npm i -g @x12i/catalox`) or linked (`npm link` from this repo). Otherwise, after `npm run build`, use:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npm run cli -- firestore report-native-layout --app "narrix"
|
|
89
|
+
# same as: node dist/src/cli/index.js firestore report-native-layout --app "narrix"
|
|
90
|
+
```
|
|
91
|
+
|
|
85
92
|
## Firestore data model (logical collections)
|
|
86
93
|
|
|
94
|
+
For a **full layout** (subcollections, document id conventions, snapshot runs, and query notes), see [`docs/firestore-data-model.md`](docs/firestore-data-model.md).
|
|
95
|
+
|
|
87
96
|
Metadata:
|
|
88
97
|
|
|
89
98
|
- `apps/{appId}`
|
|
90
99
|
- `catalogs/{catalogId}`
|
|
91
100
|
- `catalogBindings/{bindingId}` (many-to-many app↔catalog)
|
|
101
|
+
- `storeAppBindings/{bindingId}` (store↔app, multi-app export/report)
|
|
92
102
|
- `catalogDefinitions/{catalogId}` (native vs mapped specifics)
|
|
93
103
|
- `catalogAdapters/{adapterId}` (mongo/api adapter definitions)
|
|
94
104
|
- `catalogMappings/{mappingId}` (field mapping specs)
|
|
@@ -98,16 +108,24 @@ Metadata:
|
|
|
98
108
|
|
|
99
109
|
Data:
|
|
100
110
|
|
|
101
|
-
- `catalogData
|
|
111
|
+
- `catalogData-{catalogId}-items/{itemId}` (native item rows; top-level collection per catalog)
|
|
112
|
+
- `catalogData/{catalogId}` (index and metadata for that catalog’s native storage; not item payloads)
|
|
102
113
|
- `catalogSnapshots/{catalogId}/items/{itemId}` (mapped snapshot mode)
|
|
103
114
|
|
|
115
|
+
Backups (optional operator feature):
|
|
116
|
+
|
|
117
|
+
- `backup-*` and `{timestamp}__backup-*` (Firebase mode, same database)
|
|
118
|
+
- Mongo database `catalox-backups` (Mongo mode)
|
|
119
|
+
|
|
120
|
+
See [`docs/backup.md`](docs/backup.md) for `backupData` / CLI backup, [`docs/restore-firestore-backup.md`](docs/restore-firestore-backup.md) for restoring Firebase mirrors to live data with undo, [`docs/migration-native-catalog-data.md`](docs/migration-native-catalog-data.md) for moving legacy native rows into `catalogData-{catalogId}-items`, and [`docs/firestore-native-layout-vs-backup.md`](docs/firestore-native-layout-vs-backup.md) for how live paths differ from `backup-*` mirrors and what `backupData` reports in `nativeItemSourceLayoutByCatalogId`. If the console still shows **`catalogData/{catalogId}/items`**, run **`catalox firestore report-native-layout`** then **`catalox firestore migrate-native-catalog-data`** (backup + copy to flat; optional `--delete-legacy` after you trust backups).
|
|
121
|
+
|
|
104
122
|
## Core usage (generic from `appId`)
|
|
105
123
|
|
|
106
124
|
### Create stores and `Catalox`
|
|
107
125
|
|
|
108
126
|
```ts
|
|
109
127
|
import { FirestoreStore } from "@x12i/catalox";
|
|
110
|
-
import { AppStore, CatalogStore, BindingStore, DefinitionStore, MappingStore, DescriptorStore, ReferenceStore, NativeItemStore, SnapshotStore, AdapterStore } from "@x12i/catalox";
|
|
128
|
+
import { AppStore, CatalogStore, BindingStore, DefinitionStore, MappingStore, DescriptorStore, ReferenceStore, NativeItemStore, CatalogDataIndexStore, SnapshotStore, AdapterStore } from "@x12i/catalox";
|
|
111
129
|
import { AuthorizationService, Catalox } from "@x12i/catalox";
|
|
112
130
|
|
|
113
131
|
// firebase-admin initialization is up to the consumer
|
|
@@ -116,18 +134,21 @@ import { getFirestore } from "firebase-admin/firestore";
|
|
|
116
134
|
const firestore = getFirestore();
|
|
117
135
|
const store = new FirestoreStore({ firestore });
|
|
118
136
|
|
|
137
|
+
const bindings = new BindingStore(store);
|
|
119
138
|
const deps = {
|
|
120
139
|
apps: new AppStore(store),
|
|
121
140
|
catalogs: new CatalogStore(store),
|
|
122
|
-
bindings
|
|
141
|
+
bindings,
|
|
123
142
|
definitions: new DefinitionStore(store),
|
|
124
143
|
mappings: new MappingStore(store),
|
|
125
144
|
descriptors: new DescriptorStore(store),
|
|
126
145
|
references: new ReferenceStore(store),
|
|
127
146
|
nativeItems: new NativeItemStore(store),
|
|
147
|
+
catalogDataIndex: new CatalogDataIndexStore(store),
|
|
128
148
|
snapshots: new SnapshotStore(store),
|
|
129
149
|
adapters: new AdapterStore(store),
|
|
130
|
-
|
|
150
|
+
firestoreStore: store,
|
|
151
|
+
authz: new AuthorizationService(bindings),
|
|
131
152
|
};
|
|
132
153
|
|
|
133
154
|
const catalox = new Catalox(deps);
|
|
@@ -320,7 +341,7 @@ await catalox.createCatalog(ctx, {
|
|
|
320
341
|
catalogId: "signals",
|
|
321
342
|
name: "Signals",
|
|
322
343
|
sourceMode: "native",
|
|
323
|
-
native: { type: "native"
|
|
344
|
+
native: { type: "native" },
|
|
324
345
|
});
|
|
325
346
|
```
|
|
326
347
|
|
|
@@ -464,7 +485,7 @@ Integration tests are **live-only** (no mocks, no emulators). They require:
|
|
|
464
485
|
### Live test safety (read before running)
|
|
465
486
|
|
|
466
487
|
- **Never run against production credentials/projects.** Use a dedicated Firebase project for tests.
|
|
467
|
-
- **Touched collections**: `apps`, `catalogs`, `catalogBindings`, `catalogDefinitions`, `catalogDescriptors`, and `catalogData
|
|
488
|
+
- **Touched collections**: `apps`, `catalogs`, `catalogBindings`, `catalogDefinitions`, `catalogDescriptors`, `catalogData/{catalogId}`, and `catalogData-{catalogId}-items/...`. The **`catalox firestore restore-backup`** / **`undo-restore-backup`** commands additionally write `backup-restoreSessions` and `{restoreSessionId}__preRestore-*` sidecar collections (see [`docs/restore-firestore-backup.md`](docs/restore-firestore-backup.md)).
|
|
468
489
|
- **Cleanup**: tests do best-effort deletes of the docs they created, but do not guarantee full teardown.
|
|
469
490
|
|
|
470
491
|
## Boundaries (important)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type Firestore } from "firebase-admin/firestore";
|
|
2
|
+
import { type Db } from "mongodb";
|
|
3
|
+
import type { CatalogId } from "../contracts/ids.js";
|
|
4
|
+
import type { BackupDataInput, BackupDataResult } from "../contracts/backup.js";
|
|
5
|
+
import type { CatalogRecord } from "../contracts/catalogs.js";
|
|
6
|
+
import { CatalogStore } from "../firebase/catalog-store.js";
|
|
7
|
+
import { BindingStore } from "../firebase/binding-store.js";
|
|
8
|
+
import { FirestoreStore } from "../firebase/firestore-store.js";
|
|
9
|
+
export declare const BACKUP_METADATA_SOURCE_NAMES: readonly ["apps", "catalogs", "catalogBindings", "storeAppBindings", "catalogDefinitions", "catalogDescriptors", "catalogMappings", "catalogAdapters", "catalogReferences", "catalogData"];
|
|
10
|
+
export type BackupDataDeps = {
|
|
11
|
+
firestoreStore: FirestoreStore;
|
|
12
|
+
catalogs: CatalogStore;
|
|
13
|
+
bindings: BindingStore;
|
|
14
|
+
};
|
|
15
|
+
export declare function formatBackupTimestamp(versionOverride?: string, d?: Date): string;
|
|
16
|
+
/** Latest Firebase backup mirror collection id for a logical source (e.g. `apps`, `catalogData--{id}`). */
|
|
17
|
+
export declare function firebaseLatestName(logical: string): string;
|
|
18
|
+
/** Versioned Firebase backup collection id. */
|
|
19
|
+
export declare function firebaseVersionedName(ts: string, logical: string): string;
|
|
20
|
+
export declare function firebaseNativeLogical(catalogId: string): string;
|
|
21
|
+
export declare function firebaseSnapshotLogical(catalogId: string): string;
|
|
22
|
+
export declare function firebaseSnapshotRunLogical(catalogId: string, runId: string): string;
|
|
23
|
+
export declare function paginatedCopyFirestoreToFirestore(fs: Firestore, sourceCollectionPath: string, destCollectionPath: string, counts: Record<string, number>, countKey: string): Promise<void>;
|
|
24
|
+
export declare function deleteAllDocsInFirestoreCollection(fs: Firestore, collectionPath: string): Promise<void>;
|
|
25
|
+
export declare function discoverNativeCatalogIds(fs: Firestore, catalogs: CatalogRecord[], opts: {
|
|
26
|
+
bindings: BindingStore;
|
|
27
|
+
catalogIds?: CatalogId[];
|
|
28
|
+
appId?: string;
|
|
29
|
+
}): Promise<CatalogId[]>;
|
|
30
|
+
/** Delete Mongo `backupRuns` documents with createdAt older than cutoff ISO string. */
|
|
31
|
+
export declare function pruneMongoBackupRuns(db: Db, createdBeforeIso: string): Promise<number>;
|
|
32
|
+
/**
|
|
33
|
+
* Delete documents in oldest Firestore versioned backup collections matching `{ts}__backup-*`
|
|
34
|
+
* until at most `keepLast` such collections remain.
|
|
35
|
+
*/
|
|
36
|
+
export declare function pruneFirebaseVersionedBackups(firestore: Firestore, keepLast: number): Promise<{
|
|
37
|
+
deleted: string[];
|
|
38
|
+
}>;
|
|
39
|
+
export declare function runBackupData(deps: BackupDataDeps, input: BackupDataInput): Promise<BackupDataResult>;
|
|
40
|
+
//# sourceMappingURL=backup-data.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backup-data.d.ts","sourceRoot":"","sources":["../../../src/catalox/backup-data.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,SAAS,EAAE,MAAM,0BAA0B,CAAC;AACrE,OAAO,EAAe,KAAK,EAAE,EAAE,MAAM,SAAS,CAAC;AAE/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,eAAe,EAAmB,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AACjG,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAWhE,eAAO,MAAM,4BAA4B,4LAY/B,CAAC;AAEX,MAAM,MAAM,cAAc,GAAG;IAC3B,cAAc,EAAE,cAAc,CAAC;IAC/B,QAAQ,EAAE,YAAY,CAAC;IACvB,QAAQ,EAAE,YAAY,CAAC;CACxB,CAAC;AAEF,wBAAgB,qBAAqB,CAAC,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC,OAAa,GAAG,MAAM,CAMtF;AAMD,2GAA2G;AAC3G,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,+CAA+C;AAC/C,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAEzE;AA0BD,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnF;AAED,wBAAsB,iCAAiC,CACrD,EAAE,EAAE,SAAS,EACb,oBAAoB,EAAE,MAAM,EAC5B,kBAAkB,EAAE,MAAM,EAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAmBf;AAED,wBAAsB,kCAAkC,CAAC,EAAE,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAS7G;AAED,wBAAsB,wBAAwB,CAC5C,EAAE,EAAE,SAAS,EACb,QAAQ,EAAE,aAAa,EAAE,EACzB,IAAI,EAAE;IAAE,QAAQ,EAAE,YAAY,CAAC;IAAC,UAAU,CAAC,EAAE,SAAS,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GACzE,OAAO,CAAC,SAAS,EAAE,CAAC,CAwBtB;AA0PD,uFAAuF;AACvF,wBAAsB,oBAAoB,CAAC,EAAE,EAAE,EAAE,EAAE,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAK5F;AAED;;;GAGG;AACH,wBAAsB,6BAA6B,CACjD,SAAS,EAAE,SAAS,EACpB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAchC;AAED,wBAAsB,aAAa,CACjC,IAAI,EAAE,cAAc,EACpB,KAAK,EAAE,eAAe,GACrB,OAAO,CAAC,gBAAgB,CAAC,CA+K3B"}
|
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
import { FieldPath } from "firebase-admin/firestore";
|
|
2
|
+
import { MongoClient } from "mongodb";
|
|
3
|
+
import { CatalogStore } from "../firebase/catalog-store.js";
|
|
4
|
+
import { BindingStore } from "../firebase/binding-store.js";
|
|
5
|
+
import { FirestoreStore } from "../firebase/firestore-store.js";
|
|
6
|
+
import { flatNativeItemsCollectionRef, legacyNativeItemsCollectionRef, nativeItemsCollectionId, resolveNativeItemsLayout, } from "../firebase/catalog-data-paths.js";
|
|
7
|
+
const MONGO_BACKUP_DB = "catalox-backups";
|
|
8
|
+
export const BACKUP_METADATA_SOURCE_NAMES = [
|
|
9
|
+
"apps",
|
|
10
|
+
"catalogs",
|
|
11
|
+
"catalogBindings",
|
|
12
|
+
"storeAppBindings",
|
|
13
|
+
"catalogDefinitions",
|
|
14
|
+
"catalogDescriptors",
|
|
15
|
+
"catalogMappings",
|
|
16
|
+
"catalogAdapters",
|
|
17
|
+
"catalogReferences",
|
|
18
|
+
/** Parent docs only (`catalogData/{catalogId}`); subcollections are not copied. */
|
|
19
|
+
"catalogData",
|
|
20
|
+
];
|
|
21
|
+
export function formatBackupTimestamp(versionOverride, d = new Date()) {
|
|
22
|
+
if (versionOverride?.trim()) {
|
|
23
|
+
return versionOverride.trim().replace(/[/\\]/g, "_");
|
|
24
|
+
}
|
|
25
|
+
const iso = d.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
26
|
+
return iso.replace(/[-:]/g, "_").replace(/\./g, "_");
|
|
27
|
+
}
|
|
28
|
+
function firebaseTmpName(logical) {
|
|
29
|
+
return `tmp-backup-${logical}`;
|
|
30
|
+
}
|
|
31
|
+
/** Latest Firebase backup mirror collection id for a logical source (e.g. `apps`, `catalogData--{id}`). */
|
|
32
|
+
export function firebaseLatestName(logical) {
|
|
33
|
+
return `backup-${logical}`;
|
|
34
|
+
}
|
|
35
|
+
/** Versioned Firebase backup collection id. */
|
|
36
|
+
export function firebaseVersionedName(ts, logical) {
|
|
37
|
+
return `${ts}__backup-${logical}`;
|
|
38
|
+
}
|
|
39
|
+
function mongoTmpName(logical) {
|
|
40
|
+
return `__tmp_backup_${logical}`;
|
|
41
|
+
}
|
|
42
|
+
function mongoVersionedName(ts, logical) {
|
|
43
|
+
return `${ts}__${logical}`;
|
|
44
|
+
}
|
|
45
|
+
function mongoNativeItemsCollectionName(catalogId) {
|
|
46
|
+
return `catalogData__${sanitizeMongoCollectionPart(catalogId)}`;
|
|
47
|
+
}
|
|
48
|
+
function mongoSnapshotCollectionName(catalogId) {
|
|
49
|
+
return `catalogSnapshots__${sanitizeMongoCollectionPart(catalogId)}`;
|
|
50
|
+
}
|
|
51
|
+
function mongoSnapshotRunCollectionName(catalogId, runId) {
|
|
52
|
+
return `catalogSnapshotsRun__${sanitizeMongoCollectionPart(catalogId)}__${sanitizeMongoCollectionPart(runId)}`;
|
|
53
|
+
}
|
|
54
|
+
function sanitizeMongoCollectionPart(s) {
|
|
55
|
+
return String(s).replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
56
|
+
}
|
|
57
|
+
export function firebaseNativeLogical(catalogId) {
|
|
58
|
+
return `catalogData--${catalogId}`;
|
|
59
|
+
}
|
|
60
|
+
export function firebaseSnapshotLogical(catalogId) {
|
|
61
|
+
return `catalogSnapshots--${catalogId}`;
|
|
62
|
+
}
|
|
63
|
+
export function firebaseSnapshotRunLogical(catalogId, runId) {
|
|
64
|
+
return `catalogSnapshotsRun--${catalogId}--${runId}`;
|
|
65
|
+
}
|
|
66
|
+
export async function paginatedCopyFirestoreToFirestore(fs, sourceCollectionPath, destCollectionPath, counts, countKey) {
|
|
67
|
+
const src = fs.collection(sourceCollectionPath);
|
|
68
|
+
const dst = fs.collection(destCollectionPath);
|
|
69
|
+
let last;
|
|
70
|
+
let total = 0;
|
|
71
|
+
while (true) {
|
|
72
|
+
let q = src.orderBy(FieldPath.documentId()).limit(400);
|
|
73
|
+
if (last)
|
|
74
|
+
q = q.startAfter(last);
|
|
75
|
+
const snap = await q.get();
|
|
76
|
+
if (snap.empty)
|
|
77
|
+
break;
|
|
78
|
+
const batch = fs.batch();
|
|
79
|
+
for (const d of snap.docs) {
|
|
80
|
+
batch.set(dst.doc(d.id), d.data(), { merge: false });
|
|
81
|
+
total += 1;
|
|
82
|
+
}
|
|
83
|
+
await batch.commit();
|
|
84
|
+
last = snap.docs[snap.docs.length - 1];
|
|
85
|
+
}
|
|
86
|
+
counts[countKey] = (counts[countKey] ?? 0) + total;
|
|
87
|
+
}
|
|
88
|
+
export async function deleteAllDocsInFirestoreCollection(fs, collectionPath) {
|
|
89
|
+
const col = fs.collection(collectionPath);
|
|
90
|
+
while (true) {
|
|
91
|
+
const snap = await col.limit(450).get();
|
|
92
|
+
if (snap.empty)
|
|
93
|
+
break;
|
|
94
|
+
const batch = fs.batch();
|
|
95
|
+
for (const d of snap.docs)
|
|
96
|
+
batch.delete(d.ref);
|
|
97
|
+
await batch.commit();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export async function discoverNativeCatalogIds(fs, catalogs, opts) {
|
|
101
|
+
const ids = new Set();
|
|
102
|
+
for (const c of catalogs) {
|
|
103
|
+
if (c.sourceMode === "native")
|
|
104
|
+
ids.add(String(c.catalogId));
|
|
105
|
+
}
|
|
106
|
+
const parentRefs = await fs.collection("catalogData").listDocuments();
|
|
107
|
+
for (const pref of parentRefs) {
|
|
108
|
+
const cid = pref.id;
|
|
109
|
+
const leg = await pref.collection("items").limit(1).get();
|
|
110
|
+
const flat = await fs.collection(nativeItemsCollectionId(cid)).limit(1).get();
|
|
111
|
+
if (!leg.empty || !flat.empty)
|
|
112
|
+
ids.add(cid);
|
|
113
|
+
}
|
|
114
|
+
let list = [...ids];
|
|
115
|
+
if (opts.appId) {
|
|
116
|
+
const binds = await opts.bindings.listByApp(opts.appId);
|
|
117
|
+
const allowed = new Set(binds.map((b) => String(b.catalogId)));
|
|
118
|
+
list = list.filter((id) => allowed.has(id));
|
|
119
|
+
}
|
|
120
|
+
if (opts.catalogIds?.length) {
|
|
121
|
+
const allow = new Set(opts.catalogIds.map(String));
|
|
122
|
+
list = list.filter((id) => allow.has(id));
|
|
123
|
+
}
|
|
124
|
+
return list;
|
|
125
|
+
}
|
|
126
|
+
async function copyNativeCatalogToFirestoreBackup(fs, catalogId, destCollectionPath, counts, layout) {
|
|
127
|
+
const src = layout === "legacy"
|
|
128
|
+
? legacyNativeItemsCollectionRef(fs, catalogId)
|
|
129
|
+
: flatNativeItemsCollectionRef(fs, catalogId);
|
|
130
|
+
await paginatedCopyFirestoreToFirestore(fs, src.path, destCollectionPath, counts, destCollectionPath);
|
|
131
|
+
}
|
|
132
|
+
async function mongoReplacePage(col, docs) {
|
|
133
|
+
const bulk = docs.map((d) => {
|
|
134
|
+
const payload = { ...d.data() };
|
|
135
|
+
delete payload._id;
|
|
136
|
+
return {
|
|
137
|
+
replaceOne: {
|
|
138
|
+
filter: { _id: d.id },
|
|
139
|
+
replacement: { _id: d.id, ...payload },
|
|
140
|
+
upsert: true,
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
if (bulk.length)
|
|
145
|
+
await col.bulkWrite(bulk);
|
|
146
|
+
}
|
|
147
|
+
async function paginatedCopyFirestoreToMongo(fs, sourceCollectionPath, db, destCollectionName, counts, countKey) {
|
|
148
|
+
const src = fs.collection(sourceCollectionPath);
|
|
149
|
+
const col = db.collection(destCollectionName);
|
|
150
|
+
let last;
|
|
151
|
+
let total = 0;
|
|
152
|
+
while (true) {
|
|
153
|
+
let q = src.orderBy(FieldPath.documentId()).limit(200);
|
|
154
|
+
if (last)
|
|
155
|
+
q = q.startAfter(last);
|
|
156
|
+
const snap = await q.get();
|
|
157
|
+
if (snap.empty)
|
|
158
|
+
break;
|
|
159
|
+
await mongoReplacePage(col, snap.docs);
|
|
160
|
+
total += snap.docs.length;
|
|
161
|
+
last = snap.docs[snap.docs.length - 1];
|
|
162
|
+
}
|
|
163
|
+
counts[countKey] = (counts[countKey] ?? 0) + total;
|
|
164
|
+
}
|
|
165
|
+
async function promoteMongoLatest(db, tmpName, latestName) {
|
|
166
|
+
const n = await db.collection(tmpName).countDocuments();
|
|
167
|
+
if (n === 0) {
|
|
168
|
+
await db.collection(latestName).drop().catch(() => undefined);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
await db.collection(latestName).drop().catch(() => undefined);
|
|
172
|
+
await db.collection(tmpName).rename(latestName, { dropTarget: true });
|
|
173
|
+
}
|
|
174
|
+
async function backupMetadataMongo(fs, db, ts, counts, collectionsWritten, versionedWritten, issues) {
|
|
175
|
+
for (const name of BACKUP_METADATA_SOURCE_NAMES) {
|
|
176
|
+
const tmp = mongoTmpName(name);
|
|
177
|
+
const versioned = mongoVersionedName(ts, name);
|
|
178
|
+
try {
|
|
179
|
+
await db.collection(tmp).drop().catch(() => undefined);
|
|
180
|
+
await paginatedCopyFirestoreToMongo(fs, name, db, tmp, counts, tmp);
|
|
181
|
+
await db.collection(versioned).drop().catch(() => undefined);
|
|
182
|
+
await paginatedCopyFirestoreToMongo(fs, name, db, versioned, counts, versioned);
|
|
183
|
+
await promoteMongoLatest(db, tmp, name);
|
|
184
|
+
collectionsWritten.push(name);
|
|
185
|
+
versionedWritten.push(versioned);
|
|
186
|
+
}
|
|
187
|
+
catch (e) {
|
|
188
|
+
issues.push({
|
|
189
|
+
code: "mongo_metadata_copy_failed",
|
|
190
|
+
message: e instanceof Error ? e.message : String(e),
|
|
191
|
+
detail: { name },
|
|
192
|
+
});
|
|
193
|
+
throw e;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async function backupMetadataFirebase(fs, ts, counts, collectionsWritten, versionedWritten, issues) {
|
|
198
|
+
for (const name of BACKUP_METADATA_SOURCE_NAMES) {
|
|
199
|
+
const tmp = firebaseTmpName(name);
|
|
200
|
+
const latest = firebaseLatestName(name);
|
|
201
|
+
const ver = firebaseVersionedName(ts, name);
|
|
202
|
+
try {
|
|
203
|
+
await deleteAllDocsInFirestoreCollection(fs, tmp);
|
|
204
|
+
await paginatedCopyFirestoreToFirestore(fs, name, tmp, counts, tmp);
|
|
205
|
+
await paginatedCopyFirestoreToFirestore(fs, name, ver, counts, ver);
|
|
206
|
+
await deleteAllDocsInFirestoreCollection(fs, latest);
|
|
207
|
+
await paginatedCopyFirestoreToFirestore(fs, tmp, latest, counts, latest);
|
|
208
|
+
await deleteAllDocsInFirestoreCollection(fs, tmp);
|
|
209
|
+
collectionsWritten.push(latest);
|
|
210
|
+
versionedWritten.push(ver);
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
issues.push({
|
|
214
|
+
code: "firebase_metadata_copy_failed",
|
|
215
|
+
message: e instanceof Error ? e.message : String(e),
|
|
216
|
+
detail: { name },
|
|
217
|
+
});
|
|
218
|
+
throw e;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async function backupSnapshotsFirebase(fs, ts, counts, collectionsWritten, versionedWritten, issues) {
|
|
223
|
+
const roots = await fs.collection("catalogSnapshots").listDocuments();
|
|
224
|
+
for (const pref of roots) {
|
|
225
|
+
const catalogId = pref.id;
|
|
226
|
+
const logical = firebaseSnapshotLogical(catalogId);
|
|
227
|
+
const itemsPath = pref.collection("items").path;
|
|
228
|
+
const tmp = firebaseTmpName(logical);
|
|
229
|
+
const latest = firebaseLatestName(logical);
|
|
230
|
+
const ver = firebaseVersionedName(ts, logical);
|
|
231
|
+
try {
|
|
232
|
+
await deleteAllDocsInFirestoreCollection(fs, tmp);
|
|
233
|
+
await paginatedCopyFirestoreToFirestore(fs, itemsPath, tmp, counts, tmp);
|
|
234
|
+
await paginatedCopyFirestoreToFirestore(fs, itemsPath, ver, counts, ver);
|
|
235
|
+
await deleteAllDocsInFirestoreCollection(fs, latest);
|
|
236
|
+
await paginatedCopyFirestoreToFirestore(fs, tmp, latest, counts, latest);
|
|
237
|
+
await deleteAllDocsInFirestoreCollection(fs, tmp);
|
|
238
|
+
collectionsWritten.push(latest);
|
|
239
|
+
versionedWritten.push(ver);
|
|
240
|
+
const runs = await pref.collection("runs").listDocuments();
|
|
241
|
+
for (const runRef of runs) {
|
|
242
|
+
const runId = runRef.id;
|
|
243
|
+
const runLogical = firebaseSnapshotRunLogical(catalogId, runId);
|
|
244
|
+
const runItemsPath = runRef.collection("items").path;
|
|
245
|
+
const rtmp = firebaseTmpName(runLogical);
|
|
246
|
+
const rlatest = firebaseLatestName(runLogical);
|
|
247
|
+
const rver = firebaseVersionedName(ts, runLogical);
|
|
248
|
+
await deleteAllDocsInFirestoreCollection(fs, rtmp);
|
|
249
|
+
await paginatedCopyFirestoreToFirestore(fs, runItemsPath, rtmp, counts, rtmp);
|
|
250
|
+
await paginatedCopyFirestoreToFirestore(fs, runItemsPath, rver, counts, rver);
|
|
251
|
+
await deleteAllDocsInFirestoreCollection(fs, rlatest);
|
|
252
|
+
await paginatedCopyFirestoreToFirestore(fs, rtmp, rlatest, counts, rlatest);
|
|
253
|
+
await deleteAllDocsInFirestoreCollection(fs, rtmp);
|
|
254
|
+
collectionsWritten.push(rlatest);
|
|
255
|
+
versionedWritten.push(rver);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch (e) {
|
|
259
|
+
issues.push({
|
|
260
|
+
code: "firebase_snapshot_copy_failed",
|
|
261
|
+
message: e instanceof Error ? e.message : String(e),
|
|
262
|
+
detail: { catalogId },
|
|
263
|
+
});
|
|
264
|
+
throw e;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
async function backupSnapshotsMongo(fs, db, ts, counts, collectionsWritten, versionedWritten, issues) {
|
|
269
|
+
const roots = await fs.collection("catalogSnapshots").listDocuments();
|
|
270
|
+
for (const pref of roots) {
|
|
271
|
+
const catalogId = pref.id;
|
|
272
|
+
const itemsPath = pref.collection("items").path;
|
|
273
|
+
const latestName = mongoSnapshotCollectionName(catalogId);
|
|
274
|
+
const versionedName = mongoVersionedName(ts, latestName);
|
|
275
|
+
const tmpName = mongoTmpName(latestName);
|
|
276
|
+
try {
|
|
277
|
+
await db.collection(tmpName).drop().catch(() => undefined);
|
|
278
|
+
await paginatedCopyFirestoreToMongo(fs, itemsPath, db, tmpName, counts, tmpName);
|
|
279
|
+
await db.collection(versionedName).drop().catch(() => undefined);
|
|
280
|
+
await paginatedCopyFirestoreToMongo(fs, itemsPath, db, versionedName, counts, versionedName);
|
|
281
|
+
await promoteMongoLatest(db, tmpName, latestName);
|
|
282
|
+
collectionsWritten.push(latestName);
|
|
283
|
+
versionedWritten.push(versionedName);
|
|
284
|
+
const runs = await pref.collection("runs").listDocuments();
|
|
285
|
+
for (const runRef of runs) {
|
|
286
|
+
const runId = runRef.id;
|
|
287
|
+
const runItemsPath = runRef.collection("items").path;
|
|
288
|
+
const runLatest = mongoSnapshotRunCollectionName(catalogId, runId);
|
|
289
|
+
const runVer = mongoVersionedName(ts, runLatest);
|
|
290
|
+
const runTmp = mongoTmpName(runLatest);
|
|
291
|
+
await db.collection(runTmp).drop().catch(() => undefined);
|
|
292
|
+
await paginatedCopyFirestoreToMongo(fs, runItemsPath, db, runTmp, counts, runTmp);
|
|
293
|
+
await db.collection(runVer).drop().catch(() => undefined);
|
|
294
|
+
await paginatedCopyFirestoreToMongo(fs, runItemsPath, db, runVer, counts, runVer);
|
|
295
|
+
await promoteMongoLatest(db, runTmp, runLatest);
|
|
296
|
+
collectionsWritten.push(runLatest);
|
|
297
|
+
versionedWritten.push(runVer);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
catch (e) {
|
|
301
|
+
issues.push({
|
|
302
|
+
code: "mongo_snapshot_copy_failed",
|
|
303
|
+
message: e instanceof Error ? e.message : String(e),
|
|
304
|
+
detail: { catalogId },
|
|
305
|
+
});
|
|
306
|
+
throw e;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
async function writeBackupManifestFirebase(fs, payload) {
|
|
311
|
+
await fs.collection("backup-backupRuns").add({
|
|
312
|
+
...payload,
|
|
313
|
+
createdAt: new Date().toISOString(),
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
async function writeBackupManifestMongo(db, payload) {
|
|
317
|
+
await db.collection("backupRuns").insertOne({
|
|
318
|
+
...payload,
|
|
319
|
+
createdAt: new Date().toISOString(),
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
/** Delete Mongo `backupRuns` documents with createdAt older than cutoff ISO string. */
|
|
323
|
+
export async function pruneMongoBackupRuns(db, createdBeforeIso) {
|
|
324
|
+
const res = await db.collection("backupRuns").deleteMany({
|
|
325
|
+
createdAt: { $lt: createdBeforeIso },
|
|
326
|
+
});
|
|
327
|
+
return res.deletedCount ?? 0;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Delete documents in oldest Firestore versioned backup collections matching `{ts}__backup-*`
|
|
331
|
+
* until at most `keepLast` such collections remain.
|
|
332
|
+
*/
|
|
333
|
+
export async function pruneFirebaseVersionedBackups(firestore, keepLast) {
|
|
334
|
+
const cols = await firestore.listCollections();
|
|
335
|
+
const versioned = cols
|
|
336
|
+
.map((c) => c.id)
|
|
337
|
+
.filter((id) => /^\d{4}_\d{2}_\d{2}T.*__backup-/.test(id))
|
|
338
|
+
.sort();
|
|
339
|
+
if (versioned.length <= keepLast)
|
|
340
|
+
return { deleted: [] };
|
|
341
|
+
const toRemove = versioned.slice(0, versioned.length - keepLast);
|
|
342
|
+
const deleted = [];
|
|
343
|
+
for (const id of toRemove) {
|
|
344
|
+
await deleteAllDocsInFirestoreCollection(firestore, id);
|
|
345
|
+
deleted.push(id);
|
|
346
|
+
}
|
|
347
|
+
return { deleted };
|
|
348
|
+
}
|
|
349
|
+
export async function runBackupData(deps, input) {
|
|
350
|
+
const issues = [];
|
|
351
|
+
const collectionsWritten = [];
|
|
352
|
+
const versionedCollectionsWritten = [];
|
|
353
|
+
const counts = {};
|
|
354
|
+
const nativeItemSourceLayoutByCatalogId = {};
|
|
355
|
+
const fs = deps.firestoreStore.firestore;
|
|
356
|
+
const ts = formatBackupTimestamp(input.versionTimestamp);
|
|
357
|
+
const includeSnapshots = Boolean(input.includeSnapshots);
|
|
358
|
+
const allCatalogs = await deps.catalogs.listAll();
|
|
359
|
+
const nativeCatalogIds = await discoverNativeCatalogIds(fs, allCatalogs, {
|
|
360
|
+
bindings: deps.bindings,
|
|
361
|
+
...(input.catalogIds != null && input.catalogIds.length ? { catalogIds: input.catalogIds } : {}),
|
|
362
|
+
...(input.appId != null && input.appId !== "" ? { appId: input.appId } : {}),
|
|
363
|
+
});
|
|
364
|
+
if (input.mode === "mongo") {
|
|
365
|
+
if (!input.mongoBackupUri?.trim()) {
|
|
366
|
+
return {
|
|
367
|
+
ok: false,
|
|
368
|
+
mode: "mongo",
|
|
369
|
+
databaseName: MONGO_BACKUP_DB,
|
|
370
|
+
backupTimestamp: new Date().toISOString(),
|
|
371
|
+
...(input.backupLabel != null ? { backupLabel: input.backupLabel } : {}),
|
|
372
|
+
includeSnapshots,
|
|
373
|
+
collectionsWritten: [],
|
|
374
|
+
versionedCollectionsWritten: [],
|
|
375
|
+
counts: {},
|
|
376
|
+
issues: [{ code: "missing_mongo_uri", message: "mongoBackupUri is required for mongo mode" }],
|
|
377
|
+
nativeItemSourceLayoutByCatalogId: {},
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
const client = new MongoClient(input.mongoBackupUri);
|
|
381
|
+
try {
|
|
382
|
+
await client.connect();
|
|
383
|
+
const db = client.db(MONGO_BACKUP_DB);
|
|
384
|
+
await backupMetadataMongo(fs, db, ts, counts, collectionsWritten, versionedCollectionsWritten, issues);
|
|
385
|
+
for (const catalogId of nativeCatalogIds) {
|
|
386
|
+
const layout = await resolveNativeItemsLayout(fs, catalogId);
|
|
387
|
+
nativeItemSourceLayoutByCatalogId[String(catalogId)] = layout;
|
|
388
|
+
const logical = mongoNativeItemsCollectionName(String(catalogId));
|
|
389
|
+
const tmp = mongoTmpName(logical);
|
|
390
|
+
const versioned = mongoVersionedName(ts, logical);
|
|
391
|
+
await db.collection(tmp).drop().catch(() => undefined);
|
|
392
|
+
await copyNativeCatalogToMongo(fs, db, catalogId, tmp, counts, layout);
|
|
393
|
+
await db.collection(versioned).drop().catch(() => undefined);
|
|
394
|
+
await copyNativeCatalogToMongo(fs, db, catalogId, versioned, counts, layout);
|
|
395
|
+
await promoteMongoLatest(db, tmp, logical);
|
|
396
|
+
collectionsWritten.push(logical);
|
|
397
|
+
versionedCollectionsWritten.push(versioned);
|
|
398
|
+
}
|
|
399
|
+
if (includeSnapshots) {
|
|
400
|
+
await backupSnapshotsMongo(fs, db, ts, counts, collectionsWritten, versionedCollectionsWritten, issues);
|
|
401
|
+
}
|
|
402
|
+
await writeBackupManifestMongo(db, {
|
|
403
|
+
backupTimestamp: new Date().toISOString(),
|
|
404
|
+
mode: "mongo",
|
|
405
|
+
backupLabel: input.backupLabel,
|
|
406
|
+
includeSnapshots,
|
|
407
|
+
collectionsWritten,
|
|
408
|
+
versionedCollectionsWritten,
|
|
409
|
+
counts,
|
|
410
|
+
issues,
|
|
411
|
+
nativeItemSourceLayoutByCatalogId,
|
|
412
|
+
});
|
|
413
|
+
return {
|
|
414
|
+
ok: true,
|
|
415
|
+
mode: "mongo",
|
|
416
|
+
databaseName: MONGO_BACKUP_DB,
|
|
417
|
+
backupTimestamp: new Date().toISOString(),
|
|
418
|
+
...(input.backupLabel != null ? { backupLabel: input.backupLabel } : {}),
|
|
419
|
+
includeSnapshots,
|
|
420
|
+
collectionsWritten,
|
|
421
|
+
versionedCollectionsWritten,
|
|
422
|
+
counts,
|
|
423
|
+
issues,
|
|
424
|
+
nativeItemSourceLayoutByCatalogId,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
catch (e) {
|
|
428
|
+
issues.push({
|
|
429
|
+
code: "mongo_backup_failed",
|
|
430
|
+
message: e instanceof Error ? e.message : String(e),
|
|
431
|
+
});
|
|
432
|
+
return {
|
|
433
|
+
ok: false,
|
|
434
|
+
mode: "mongo",
|
|
435
|
+
databaseName: MONGO_BACKUP_DB,
|
|
436
|
+
backupTimestamp: new Date().toISOString(),
|
|
437
|
+
...(input.backupLabel != null ? { backupLabel: input.backupLabel } : {}),
|
|
438
|
+
includeSnapshots,
|
|
439
|
+
collectionsWritten,
|
|
440
|
+
versionedCollectionsWritten,
|
|
441
|
+
counts,
|
|
442
|
+
issues,
|
|
443
|
+
nativeItemSourceLayoutByCatalogId,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
finally {
|
|
447
|
+
await client.close().catch(() => undefined);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
try {
|
|
451
|
+
await backupMetadataFirebase(fs, ts, counts, collectionsWritten, versionedCollectionsWritten, issues);
|
|
452
|
+
for (const catalogId of nativeCatalogIds) {
|
|
453
|
+
const layout = await resolveNativeItemsLayout(fs, catalogId);
|
|
454
|
+
nativeItemSourceLayoutByCatalogId[String(catalogId)] = layout;
|
|
455
|
+
const logical = firebaseNativeLogical(String(catalogId));
|
|
456
|
+
const tmp = firebaseTmpName(logical);
|
|
457
|
+
const latest = firebaseLatestName(logical);
|
|
458
|
+
const ver = firebaseVersionedName(ts, logical);
|
|
459
|
+
await deleteAllDocsInFirestoreCollection(fs, tmp);
|
|
460
|
+
await copyNativeCatalogToFirestoreBackup(fs, catalogId, tmp, counts, layout);
|
|
461
|
+
await copyNativeCatalogToFirestoreBackup(fs, catalogId, ver, counts, layout);
|
|
462
|
+
await deleteAllDocsInFirestoreCollection(fs, latest);
|
|
463
|
+
await paginatedCopyFirestoreToFirestore(fs, tmp, latest, counts, latest);
|
|
464
|
+
await deleteAllDocsInFirestoreCollection(fs, tmp);
|
|
465
|
+
collectionsWritten.push(latest);
|
|
466
|
+
versionedCollectionsWritten.push(ver);
|
|
467
|
+
}
|
|
468
|
+
if (includeSnapshots) {
|
|
469
|
+
await backupSnapshotsFirebase(fs, ts, counts, collectionsWritten, versionedCollectionsWritten, issues);
|
|
470
|
+
}
|
|
471
|
+
await writeBackupManifestFirebase(fs, {
|
|
472
|
+
backupTimestamp: new Date().toISOString(),
|
|
473
|
+
mode: "firebase",
|
|
474
|
+
backupLabel: input.backupLabel,
|
|
475
|
+
includeSnapshots,
|
|
476
|
+
collectionsWritten,
|
|
477
|
+
versionedCollectionsWritten,
|
|
478
|
+
counts,
|
|
479
|
+
issues,
|
|
480
|
+
nativeItemSourceLayoutByCatalogId,
|
|
481
|
+
});
|
|
482
|
+
return {
|
|
483
|
+
ok: true,
|
|
484
|
+
mode: "firebase",
|
|
485
|
+
databaseName: "(same-firestore-target)",
|
|
486
|
+
backupTimestamp: new Date().toISOString(),
|
|
487
|
+
...(input.backupLabel != null ? { backupLabel: input.backupLabel } : {}),
|
|
488
|
+
includeSnapshots,
|
|
489
|
+
collectionsWritten,
|
|
490
|
+
versionedCollectionsWritten,
|
|
491
|
+
counts,
|
|
492
|
+
issues,
|
|
493
|
+
nativeItemSourceLayoutByCatalogId,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
catch (e) {
|
|
497
|
+
issues.push({
|
|
498
|
+
code: "firebase_backup_failed",
|
|
499
|
+
message: e instanceof Error ? e.message : String(e),
|
|
500
|
+
});
|
|
501
|
+
return {
|
|
502
|
+
ok: false,
|
|
503
|
+
mode: "firebase",
|
|
504
|
+
databaseName: "(same-firestore-target)",
|
|
505
|
+
backupTimestamp: new Date().toISOString(),
|
|
506
|
+
...(input.backupLabel != null ? { backupLabel: input.backupLabel } : {}),
|
|
507
|
+
includeSnapshots,
|
|
508
|
+
collectionsWritten,
|
|
509
|
+
versionedCollectionsWritten,
|
|
510
|
+
counts,
|
|
511
|
+
issues,
|
|
512
|
+
nativeItemSourceLayoutByCatalogId,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
async function copyNativeCatalogToMongo(fs, db, catalogId, destCollection, counts, layout) {
|
|
517
|
+
const src = layout === "legacy"
|
|
518
|
+
? legacyNativeItemsCollectionRef(fs, catalogId)
|
|
519
|
+
: flatNativeItemsCollectionRef(fs, catalogId);
|
|
520
|
+
await paginatedCopyFirestoreToMongo(fs, src.path, db, destCollection, counts, destCollection);
|
|
521
|
+
}
|
|
522
|
+
//# sourceMappingURL=backup-data.js.map
|