@x12i/catalox 3.0.0 → 3.1.1
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 +38 -5
- package/dist/src/catalox/authorization.js +1 -1
- package/dist/src/catalox/authorization.js.map +1 -1
- package/dist/src/catalox/catalog-lifecycle.d.ts +29 -0
- package/dist/src/catalox/catalog-lifecycle.d.ts.map +1 -0
- package/dist/src/catalox/catalog-lifecycle.js +480 -0
- package/dist/src/catalox/catalog-lifecycle.js.map +1 -0
- package/dist/src/catalox/catalox-bound.d.ts +20 -4
- package/dist/src/catalox/catalox-bound.d.ts.map +1 -1
- package/dist/src/catalox/catalox-bound.js +30 -6
- package/dist/src/catalox/catalox-bound.js.map +1 -1
- package/dist/src/catalox/catalox.d.ts +29 -4
- package/dist/src/catalox/catalox.d.ts.map +1 -1
- package/dist/src/catalox/catalox.js +483 -66
- package/dist/src/catalox/catalox.js.map +1 -1
- package/dist/src/catalox/context.js +2 -2
- package/dist/src/catalox/context.js.map +1 -1
- package/dist/src/catalox/create-catalox.d.ts +6 -0
- package/dist/src/catalox/create-catalox.d.ts.map +1 -1
- package/dist/src/catalox/create-catalox.js +25 -0
- package/dist/src/catalox/create-catalox.js.map +1 -1
- package/dist/src/catalox/index.d.ts +2 -0
- package/dist/src/catalox/index.d.ts.map +1 -1
- package/dist/src/catalox/index.js +2 -0
- package/dist/src/catalox/index.js.map +1 -1
- package/dist/src/catalox/native-catalog-merge.d.ts +12 -0
- package/dist/src/catalox/native-catalog-merge.d.ts.map +1 -0
- package/dist/src/catalox/native-catalog-merge.js +102 -0
- package/dist/src/catalox/native-catalog-merge.js.map +1 -0
- package/dist/src/catalox/native-scope.d.ts +28 -0
- package/dist/src/catalox/native-scope.d.ts.map +1 -0
- package/dist/src/catalox/native-scope.js +184 -0
- package/dist/src/catalox/native-scope.js.map +1 -0
- package/dist/src/catalox/record-history.d.ts +53 -0
- package/dist/src/catalox/record-history.d.ts.map +1 -0
- package/dist/src/catalox/record-history.js +158 -0
- package/dist/src/catalox/record-history.js.map +1 -0
- package/dist/src/cli/index.js +133 -1
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/contracts/apps.d.ts +2 -0
- package/dist/src/contracts/apps.d.ts.map +1 -1
- package/dist/src/contracts/catalog-lifecycle.d.ts +70 -0
- package/dist/src/contracts/catalog-lifecycle.d.ts.map +1 -0
- package/dist/src/contracts/catalog-lifecycle.js +2 -0
- package/dist/src/contracts/catalog-lifecycle.js.map +1 -0
- package/dist/src/contracts/catalogs.d.ts +37 -0
- package/dist/src/contracts/catalogs.d.ts.map +1 -1
- package/dist/src/contracts/catalogs.js.map +1 -1
- package/dist/src/contracts/context.d.ts +5 -1
- package/dist/src/contracts/context.d.ts.map +1 -1
- package/dist/src/contracts/descriptors.d.ts +6 -0
- package/dist/src/contracts/descriptors.d.ts.map +1 -1
- package/dist/src/contracts/index.d.ts +5 -2
- package/dist/src/contracts/index.d.ts.map +1 -1
- package/dist/src/contracts/index.js.map +1 -1
- package/dist/src/contracts/items.d.ts +19 -0
- package/dist/src/contracts/items.d.ts.map +1 -1
- package/dist/src/contracts/record-history.d.ts +66 -0
- package/dist/src/contracts/record-history.d.ts.map +1 -0
- package/dist/src/contracts/record-history.js +2 -0
- package/dist/src/contracts/record-history.js.map +1 -0
- package/dist/src/firebase/adapter-store.d.ts +1 -0
- package/dist/src/firebase/adapter-store.d.ts.map +1 -1
- package/dist/src/firebase/adapter-store.js +3 -0
- package/dist/src/firebase/adapter-store.js.map +1 -1
- package/dist/src/firebase/binding-store.d.ts +2 -0
- package/dist/src/firebase/binding-store.d.ts.map +1 -1
- package/dist/src/firebase/binding-store.js +10 -0
- package/dist/src/firebase/binding-store.js.map +1 -1
- package/dist/src/firebase/catalog-data-index-store.d.ts +1 -0
- package/dist/src/firebase/catalog-data-index-store.d.ts.map +1 -1
- package/dist/src/firebase/catalog-data-index-store.js +3 -0
- package/dist/src/firebase/catalog-data-index-store.js.map +1 -1
- package/dist/src/firebase/catalog-item-history-store.d.ts +21 -0
- package/dist/src/firebase/catalog-item-history-store.d.ts.map +1 -0
- package/dist/src/firebase/catalog-item-history-store.js +61 -0
- package/dist/src/firebase/catalog-item-history-store.js.map +1 -0
- package/dist/src/firebase/catalog-store.d.ts +1 -0
- package/dist/src/firebase/catalog-store.d.ts.map +1 -1
- package/dist/src/firebase/catalog-store.js +3 -0
- package/dist/src/firebase/catalog-store.js.map +1 -1
- package/dist/src/firebase/index.d.ts +1 -0
- package/dist/src/firebase/index.d.ts.map +1 -1
- package/dist/src/firebase/index.js +1 -0
- package/dist/src/firebase/index.js.map +1 -1
- package/dist/src/firebase/mapping-store.d.ts +1 -0
- package/dist/src/firebase/mapping-store.d.ts.map +1 -1
- package/dist/src/firebase/mapping-store.js +3 -0
- package/dist/src/firebase/mapping-store.js.map +1 -1
- package/dist/src/firebase/native-item-store.d.ts +8 -2
- package/dist/src/firebase/native-item-store.d.ts.map +1 -1
- package/dist/src/firebase/native-item-store.js +22 -6
- package/dist/src/firebase/native-item-store.js.map +1 -1
- package/dist/src/firebase/reference-store.d.ts +3 -0
- package/dist/src/firebase/reference-store.d.ts.map +1 -1
- package/dist/src/firebase/reference-store.js +16 -0
- package/dist/src/firebase/reference-store.js.map +1 -1
- package/dist/src/firebase/renderer-snippet-store.d.ts +3 -0
- package/dist/src/firebase/renderer-snippet-store.d.ts.map +1 -1
- package/dist/src/firebase/renderer-snippet-store.js +17 -0
- package/dist/src/firebase/renderer-snippet-store.js.map +1 -1
- package/dist/src/firebase/snapshot-store.d.ts +1 -0
- package/dist/src/firebase/snapshot-store.d.ts.map +1 -1
- package/dist/src/firebase/snapshot-store.js +8 -0
- package/dist/src/firebase/snapshot-store.js.map +1 -1
- package/dist/test/integration/firestore.emulator.test.js +7 -1
- package/dist/test/integration/firestore.emulator.test.js.map +1 -1
- package/dist/test/integration/record-history.live.test.d.ts +2 -0
- package/dist/test/integration/record-history.live.test.d.ts.map +1 -0
- package/dist/test/integration/record-history.live.test.js +126 -0
- package/dist/test/integration/record-history.live.test.js.map +1 -0
- package/dist/test/unit/native-catalog-merge.test.d.ts +2 -0
- package/dist/test/unit/native-catalog-merge.test.d.ts.map +1 -0
- package/dist/test/unit/native-catalog-merge.test.js +33 -0
- package/dist/test/unit/native-catalog-merge.test.js.map +1 -0
- package/dist/test/unit/native-scope.test.d.ts +2 -0
- package/dist/test/unit/native-scope.test.d.ts.map +1 -0
- package/dist/test/unit/native-scope.test.js +29 -0
- package/dist/test/unit/native-scope.test.js.map +1 -0
- package/dist/test/unit/record-history-path.test.d.ts +2 -0
- package/dist/test/unit/record-history-path.test.d.ts.map +1 -0
- package/dist/test/unit/record-history-path.test.js +24 -0
- package/dist/test/unit/record-history-path.test.js.map +1 -0
- package/firestore.indexes.json +39 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@ Catalox is a **data-tier** package for managing **app-scoped catalogs** in Fireb
|
|
|
8
8
|
- **Mapped catalogs** (items normalized from MongoDB or APIs)
|
|
9
9
|
- **References + validation contracts** (standardized cross-catalog shapes)
|
|
10
10
|
- **Seed/import/export + batch upsert** workflows for native catalogs
|
|
11
|
+
- **Optional per-record native history** — NDJSON payloads in GCS plus a Firestore `catalogItemHistory` index; list/show/restore/replay APIs and `catalox history …` ([`docs/record-history.md`](docs/record-history.md))
|
|
12
|
+
- **Optional catalog lifecycle** — hard delete (with manifest), restore from manifest, and hard rename across collections; APIs + `catalox catalog …` ([`docs/catalog-crud.md`](docs/catalog-crud.md))
|
|
11
13
|
- **Optional GCS tools** — NDJSON export/import of Firestore collections to a bucket, **compare** live data vs bucket snapshots, **`backupData` mode `gcs`** (Catalox-shaped NDJSON + manifest under a bucket prefix), and matching CLI commands (`firestore export-gcs`, `import-gcs`, `compare-gcs`, `firestore backup --mode gcs`; see [`docs/firestore-gcs-export.md`](docs/firestore-gcs-export.md), [`docs/backup.md`](docs/backup.md))
|
|
12
14
|
|
|
13
15
|
Catalox **does not** own UI, workflow orchestration, remote execution, artifact blobs, or secret storage.
|
|
@@ -83,6 +85,9 @@ GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
|
|
|
83
85
|
FIREBASE_SERVICE_ACCOUNT_PATH=/path/to/service-account.json
|
|
84
86
|
FIREBASE_PROJECT_ID=your-project-id
|
|
85
87
|
FIRESTORE_LIVE_TESTS=1
|
|
88
|
+
# Optional: record-history live test + CLI (`createCatalox`); use a dedicated test bucket:
|
|
89
|
+
# CATALOX_RECORD_HISTORY_BUCKET=your-test-bucket
|
|
90
|
+
# CATALOX_RECORD_HISTORY_PREFIX=catalox-record-history/
|
|
86
91
|
# Optional defaults for the packaged `catalox` CLI (Catalox app/store context, not GCP):
|
|
87
92
|
# CATALOX_APP_ID=myAppId
|
|
88
93
|
# CATALOX_STORE_ID=myStoreId
|
|
@@ -98,6 +103,9 @@ The `catalox` binary loads `.env` via `dotenv`. Use **`CATALOX_*`** for **Catalo
|
|
|
98
103
|
- **`CATALOX_STORE_ID`** — Default `storeId` for **`report`** and **`export`** when `--store` is omitted (`--store` overrides).
|
|
99
104
|
- **`CATALOX_USER_ID`** — Optional user id / actor for authz-sensitive CLI paths.
|
|
100
105
|
- **`CATALOX_MONGO_URI`** — If set, enables the Mongo catalog adapter in the CLI (wired via **`createCatalox`** in `src/catalox/create-catalox.ts`, called from `src/cli/index.ts`).
|
|
106
|
+
- **`CATALOX_RECORD_HISTORY_BUCKET`** — If set, **`createCatalox`** wires **`recordHistory`** (GCS payloads + **`catalogItemHistory`** on native writes; also required for **`catalox history …`** to persist new events).
|
|
107
|
+
- **`CATALOX_RECORD_HISTORY_PREFIX`** — Optional GCS prefix for record history (default `catalox-record-history/`).
|
|
108
|
+
- **`CATALOX_RECORD_HISTORY_FAIL_CLOSED`** — When **`1`**, failed history writes fail the parent Firestore mutation.
|
|
101
109
|
|
|
102
110
|
**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:
|
|
103
111
|
|
|
@@ -120,6 +128,13 @@ npm run cli -- firestore report-native-layout --app "narrix"
|
|
|
120
128
|
| `firestore export-gcs` / `import-gcs` | NDJSON per collection + optional manifest ([`docs/firestore-gcs-export.md`](docs/firestore-gcs-export.md)). |
|
|
121
129
|
| `firestore compare-gcs` | Diff live Firestore vs bucket NDJSON (single collection or manifest). |
|
|
122
130
|
|
|
131
|
+
#### `history` / `catalog` CLI (3.1+)
|
|
132
|
+
|
|
133
|
+
| Command | Purpose |
|
|
134
|
+
|---------|---------|
|
|
135
|
+
| `history list` / `history show` / `history restore` / `history replay` | Per-record native history from **`catalogItemHistory`** + GCS ([`docs/record-history.md`](docs/record-history.md)). |
|
|
136
|
+
| `catalog delete` / `catalog restore` / `catalog rename` | Hard delete with manifest, restore from manifest, or hard rename ([`docs/catalog-crud.md`](docs/catalog-crud.md)). |
|
|
137
|
+
|
|
123
138
|
## Firestore data model (logical collections)
|
|
124
139
|
|
|
125
140
|
For a **full layout** (subcollections, document id conventions, snapshot runs, and query notes), see [`docs/firestore-data-model.md`](docs/firestore-data-model.md). For **native** catalogs specifically (per-catalog `catalogData-*-items`, layout resolution, `listCatalogItems`, filters, troubleshooting), see [`docs/native-catalog-storage-and-api.md`](docs/native-catalog-storage-and-api.md).
|
|
@@ -136,6 +151,7 @@ Metadata:
|
|
|
136
151
|
- `catalogDescriptors/{catalogId}` (**descriptor metadata** for generic consumption)
|
|
137
152
|
- `catalogRendererSnippets/{catalogId}:{role}[:{mode}]` (**stored renderer snippets** for list/grid/item/report/dashboard rendering)
|
|
138
153
|
- `catalogReferences/{referenceId}` (standardized reference records)
|
|
154
|
+
- `catalogItemHistory/{eventId}` (optional **native write history** index; payloads in GCS — [`docs/record-history.md`](docs/record-history.md))
|
|
139
155
|
|
|
140
156
|
Data:
|
|
141
157
|
|
|
@@ -162,7 +178,11 @@ import { createCatalox } from "@x12i/catalox";
|
|
|
162
178
|
import { getFirestore } from "firebase-admin/firestore";
|
|
163
179
|
|
|
164
180
|
const firestore = getFirestore();
|
|
165
|
-
const catalox = createCatalox({
|
|
181
|
+
const catalox = createCatalox({
|
|
182
|
+
firestore,
|
|
183
|
+
// Optional: per-record native history to GCS + `catalogItemHistory` (or set CATALOX_RECORD_HISTORY_BUCKET for CLI)
|
|
184
|
+
// recordHistory: { gcsBucket: "my-bucket", gcsPrefix: "catalox-record-history/" },
|
|
185
|
+
});
|
|
166
186
|
|
|
167
187
|
// Optional: bind app/tenant context once (no globals; same semantics as passing context each call).
|
|
168
188
|
const scoped = catalox.withContext({ appId: "myApp" });
|
|
@@ -355,7 +375,7 @@ Catalox includes provisioning helpers to reduce “manual Firestore wiring”.
|
|
|
355
375
|
### Create a native catalog (also seeds a minimal descriptor)
|
|
356
376
|
|
|
357
377
|
```ts
|
|
358
|
-
const ctx = { appId: "myAppId",
|
|
378
|
+
const ctx = { appId: "myAppId", superAdmin: true }; // set only after host auth; often from AppRecord.superAdminApp
|
|
359
379
|
|
|
360
380
|
await catalox.createCatalog(ctx, {
|
|
361
381
|
catalogId: "signals",
|
|
@@ -485,7 +505,7 @@ Validation APIs exist with standardized contracts, but **current behavior is min
|
|
|
485
505
|
|
|
486
506
|
## Publishing
|
|
487
507
|
|
|
488
|
-
From a clean tree, **`npm publish`** runs **`prepublishOnly`**, which executes **`npm run build`** then **`npm test`** (unit tests). Ensure `dist/` is build output only; the published tarball includes **`dist`**, **`README.md`**, and **`
|
|
508
|
+
From a clean tree, **`npm publish`** runs **`prepublishOnly`**, which executes **`npm run build`** then **`npm test`** (unit tests). Ensure `dist/` is build output only; the published tarball includes **`dist`**, **`README.md`**, **`LICENSE`**, and **`firestore.indexes.json`** (composite indexes for `catalogItemHistory` queries) per `package.json` **`files`**.
|
|
489
509
|
|
|
490
510
|
## Tests
|
|
491
511
|
|
|
@@ -507,16 +527,29 @@ Integration tests are **live-only** (no mocks, no emulators). They require:
|
|
|
507
527
|
- `FIREBASE_SERVICE_ACCOUNT_PATH=...`
|
|
508
528
|
- `FIREBASE_PROJECT_ID=...`
|
|
509
529
|
|
|
510
|
-
The **`Firestore live integration`** test (`test/integration/firestore.emulator.test.ts`) uses **`createCatalox`** and asserts v3 contracts: **`listCatalogItems`** returns **`listOutcome: "ok"`**, and **`getCatalogItem`** returns **`{ outcome: "found", item }`** for the seeded row.
|
|
530
|
+
The **`Firestore live integration`** test (`test/integration/firestore.emulator.test.ts`) uses **`createCatalox`** and asserts v3 contracts: **`listCatalogItems`** returns **`listOutcome: "ok"`**, and **`getCatalogItem`** returns **`{ outcome: "found", item }`** for the seeded row. The descriptor patch in that test includes **`queryableFields` with `indexed: true`** for filtered fields so **`deriveIndexed`** persists **`indexed.*`** (see **Canonical `indexed` rule** above).
|
|
531
|
+
|
|
532
|
+
When **`CATALOX_RECORD_HISTORY_BUCKET`** is set, **`test/integration/record-history.live.test.ts`** runs as well: ephemeral app/catalog, **`upsert` → `update` → `listCatalogItemHistory` → `getCatalogItemHistoryEvent` → `restoreCatalogItemFromHistory`**, then best-effort Firestore + GCS cleanup for the created history objects.
|
|
511
533
|
|
|
512
534
|
### Live test safety (read before running)
|
|
513
535
|
|
|
514
536
|
- **Never run against production credentials/projects.** Use a dedicated Firebase project for tests.
|
|
515
|
-
- **Touched collections**: `apps`, `catalogs`, `catalogBindings`, `catalogDefinitions`, `catalogDescriptors`, `catalogData/{catalogId}`,
|
|
537
|
+
- **Touched collections**: `apps`, `catalogs`, `catalogBindings`, `catalogDefinitions`, `catalogDescriptors`, `catalogData/{catalogId}`, `catalogData-{catalogId}-items/...`, and (when record history is enabled) `catalogItemHistory/...`. 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)).
|
|
516
538
|
- **Cleanup**: tests do best-effort deletes of the docs they created, but do not guarantee full teardown.
|
|
517
539
|
|
|
518
540
|
## Changelog
|
|
519
541
|
|
|
542
|
+
### 3.1.1
|
|
543
|
+
|
|
544
|
+
- **Live tests:** Firestore integration test now sets **`queryableFields`** on the patched descriptor so equality filters match stored **`indexed.*`** rows (same rule as production descriptors).
|
|
545
|
+
- **Live tests:** Record-history integration test covers **update**, **get event**, **restore**, and **GCS object** cleanup when a bucket is configured.
|
|
546
|
+
- **Package:** publish **`firestore.indexes.json`** in the npm tarball for operators deploying **`catalogItemHistory`** queries.
|
|
547
|
+
|
|
548
|
+
### 3.1.0
|
|
549
|
+
|
|
550
|
+
- **Per-record history:** optional **`recordHistory`** on **`createCatalox`** (or **`CATALOX_RECORD_HISTORY_*`** env for CLI) writes **GCS NDJSON** + Firestore **`catalogItemHistory`** on native upsert/update/delete/batch/move. APIs: **`listCatalogItemHistory`**, **`getCatalogItemHistoryEvent`**, **`restoreCatalogItemFromHistory`**, **`replayCatalogToPointInTime`**. CLI: **`catalox history …`**. See [`docs/record-history.md`](docs/record-history.md), [`firestore.indexes.json`](firestore.indexes.json).
|
|
551
|
+
- **Catalog lifecycle:** **`deleteCatalog`**, **`restoreDeletedCatalog`**, **`renameCatalog`** (hard rename) + CLI **`catalox catalog …`**. See [`docs/catalog-crud.md`](docs/catalog-crud.md).
|
|
552
|
+
|
|
520
553
|
### 3.0.0
|
|
521
554
|
|
|
522
555
|
- **`createCatalox(config)`** — single factory for Firestore-backed stores, authz, optional Mongo/API adapters, optional renderer snippet store (`src/catalox/create-catalox.ts`).
|
|
@@ -6,7 +6,7 @@ export class AuthorizationService {
|
|
|
6
6
|
this.bindings = bindings;
|
|
7
7
|
}
|
|
8
8
|
async requireBindingAccess(context, appId, catalogId, required) {
|
|
9
|
-
if (context.
|
|
9
|
+
if (context.superAdmin) {
|
|
10
10
|
// God-mode policy: bypass binding for read/admin checks if needed.
|
|
11
11
|
// We still return a synthetic binding for uniformity.
|
|
12
12
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authorization.js","sourceRoot":"","sources":["../../../src/catalox/authorization.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAI5D,MAAM,OAAO,oBAAoB;IACF;IAA7B,YAA6B,QAAsB;QAAtB,aAAQ,GAAR,QAAQ,CAAc;IAAG,CAAC;IAEvD,KAAK,CAAC,oBAAoB,CACxB,OAAuB,EACvB,KAAY,EACZ,SAAoB,EACpB,QAAwB;QAExB,IAAI,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"authorization.js","sourceRoot":"","sources":["../../../src/catalox/authorization.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAI5D,MAAM,OAAO,oBAAoB;IACF;IAA7B,YAA6B,QAAsB;QAAtB,aAAQ,GAAR,QAAQ,CAAc;IAAG,CAAC;IAEvD,KAAK,CAAC,oBAAoB,CACxB,OAAuB,EACvB,KAAY,EACZ,SAAoB,EACpB,QAAwB;QAExB,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;YACvB,mEAAmE;YACnE,sDAAsD;YACtD,OAAO;gBACL,SAAS,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,SAAS,CAAC,EAAE;gBAClD,KAAK;gBACL,SAAS;gBACT,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE;gBACzD,MAAM,EAAE,QAAQ;gBAChB,SAAS,EAAE,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE;gBACpC,SAAS,EAAE,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE;aACrC,CAAC;QACJ,CAAC;QAED,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,gBAAgB,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;QACvE,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;YAC5C,MAAM,IAAI,wBAAwB,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,+BAA+B,EAAE,CAAC,CAAC;QACpG,CAAC;QAED,MAAM,EAAE,GACN,QAAQ,KAAK,MAAM;YACjB,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC;YACjC,CAAC,CAAC,QAAQ,KAAK,OAAO;gBACpB,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC;gBAClC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAEzC,IAAI,CAAC,EAAE,EAAE,CAAC;YACR,MAAM,IAAI,wBAAwB,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,CAAC;QACrE,CAAC;QAED,OAAO,OAAO,CAAC;IACjB,CAAC;CACF"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { CatalogId } from "../contracts/ids.js";
|
|
2
|
+
import type { DeleteCatalogInput, DeleteCatalogResult, RenameCatalogInput, RenameCatalogResult, RestoreDeletedCatalogInput, RestoreDeletedCatalogResult } from "../contracts/catalog-lifecycle.js";
|
|
3
|
+
import type { CataloxContext } from "../contracts/context.js";
|
|
4
|
+
import { AdapterStore } from "../firebase/adapter-store.js";
|
|
5
|
+
import { CatalogDataIndexStore } from "../firebase/catalog-data-index-store.js";
|
|
6
|
+
import { DefinitionStore } from "../firebase/definition-store.js";
|
|
7
|
+
import { DescriptorStore } from "../firebase/descriptor-store.js";
|
|
8
|
+
import { MappingStore } from "../firebase/mapping-store.js";
|
|
9
|
+
import { NativeItemStore } from "../firebase/native-item-store.js";
|
|
10
|
+
import { ReferenceStore } from "../firebase/reference-store.js";
|
|
11
|
+
import { RendererSnippetStore } from "../firebase/renderer-snippet-store.js";
|
|
12
|
+
import { SnapshotStore } from "../firebase/snapshot-store.js";
|
|
13
|
+
import { type BackupDataDeps } from "./backup-data.js";
|
|
14
|
+
import { type RecordHistoryDeps } from "./record-history.js";
|
|
15
|
+
export type CatalogLifecycleDeps = BackupDataDeps & {
|
|
16
|
+
definitions: DefinitionStore;
|
|
17
|
+
descriptors: DescriptorStore;
|
|
18
|
+
mappings: MappingStore;
|
|
19
|
+
adapters: AdapterStore;
|
|
20
|
+
references: ReferenceStore;
|
|
21
|
+
nativeItems: NativeItemStore;
|
|
22
|
+
snapshots: SnapshotStore;
|
|
23
|
+
catalogDataIndex: CatalogDataIndexStore;
|
|
24
|
+
rendererSnippets?: RendererSnippetStore;
|
|
25
|
+
};
|
|
26
|
+
export declare function runDeleteCatalog(deps: CatalogLifecycleDeps, recordHistory: RecordHistoryDeps | undefined, context: CataloxContext, catalogId: CatalogId, input: DeleteCatalogInput): Promise<DeleteCatalogResult>;
|
|
27
|
+
export declare function runRestoreDeletedCatalog(deps: CatalogLifecycleDeps, recordHistory: RecordHistoryDeps, input: RestoreDeletedCatalogInput): Promise<RestoreDeletedCatalogResult>;
|
|
28
|
+
export declare function runRenameCatalog(deps: CatalogLifecycleDeps, recordHistory: RecordHistoryDeps | undefined, context: CataloxContext, fromCatalogId: CatalogId, toCatalogId: CatalogId, input: RenameCatalogInput): Promise<RenameCatalogResult>;
|
|
29
|
+
//# sourceMappingURL=catalog-lifecycle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog-lifecycle.d.ts","sourceRoot":"","sources":["../../../src/catalox/catalog-lifecycle.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAEV,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,EACnB,0BAA0B,EAC1B,2BAA2B,EAC5B,MAAM,mCAAmC,CAAC;AAQ3C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAQ9D,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAE5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAC;AAEhF,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAChE,OAAO,EAAE,oBAAoB,EAAE,MAAM,uCAAuC,CAAC;AAC7E,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EAIL,KAAK,cAAc,EACpB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAGL,KAAK,iBAAiB,EACvB,MAAM,qBAAqB,CAAC;AAG7B,MAAM,MAAM,oBAAoB,GAAG,cAAc,GAAG;IAClD,WAAW,EAAE,eAAe,CAAC;IAC7B,WAAW,EAAE,eAAe,CAAC;IAC7B,QAAQ,EAAE,YAAY,CAAC;IACvB,QAAQ,EAAE,YAAY,CAAC;IACvB,UAAU,EAAE,cAAc,CAAC;IAC3B,WAAW,EAAE,eAAe,CAAC;IAC7B,SAAS,EAAE,aAAa,CAAC;IACzB,gBAAgB,EAAE,qBAAqB,CAAC;IACxC,gBAAgB,CAAC,EAAE,oBAAoB,CAAC;CACzC,CAAC;AAwDF,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,oBAAoB,EAC1B,aAAa,EAAE,iBAAiB,GAAG,SAAS,EAC5C,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE,kBAAkB,GACxB,OAAO,CAAC,mBAAmB,CAAC,CAoL9B;AAED,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,oBAAoB,EAC1B,aAAa,EAAE,iBAAiB,EAChC,KAAK,EAAE,0BAA0B,GAChC,OAAO,CAAC,2BAA2B,CAAC,CAuFtC;AAED,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,oBAAoB,EAC1B,aAAa,EAAE,iBAAiB,GAAG,SAAS,EAC5C,OAAO,EAAE,cAAc,EACvB,aAAa,EAAE,SAAS,EACxB,WAAW,EAAE,SAAS,EACtB,KAAK,EAAE,kBAAkB,GACxB,OAAO,CAAC,mBAAmB,CAAC,CAgM9B"}
|
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import { AdapterStore } from "../firebase/adapter-store.js";
|
|
2
|
+
import { BindingStore } from "../firebase/binding-store.js";
|
|
3
|
+
import { CatalogDataIndexStore } from "../firebase/catalog-data-index-store.js";
|
|
4
|
+
import { CatalogStore } from "../firebase/catalog-store.js";
|
|
5
|
+
import { DefinitionStore } from "../firebase/definition-store.js";
|
|
6
|
+
import { DescriptorStore } from "../firebase/descriptor-store.js";
|
|
7
|
+
import { MappingStore } from "../firebase/mapping-store.js";
|
|
8
|
+
import { NativeItemStore } from "../firebase/native-item-store.js";
|
|
9
|
+
import { ReferenceStore } from "../firebase/reference-store.js";
|
|
10
|
+
import { RendererSnippetStore } from "../firebase/renderer-snippet-store.js";
|
|
11
|
+
import { SnapshotStore } from "../firebase/snapshot-store.js";
|
|
12
|
+
import { deleteAllDocsInFirestoreCollection, formatBackupTimestamp, runBackupData, } from "./backup-data.js";
|
|
13
|
+
import { buildRecordHistoryGcsClient, emitRecordHistoryEventWithCustomPath, } from "./record-history.js";
|
|
14
|
+
import { nativeItemsCollectionId } from "../firebase/catalog-data-paths.js";
|
|
15
|
+
function gcsUploadJson(gcs, rel, body) {
|
|
16
|
+
return gcs.uploadObject(rel, JSON.stringify(body, null, 2), { contentType: "application/json", resumable: false });
|
|
17
|
+
}
|
|
18
|
+
function gcsUploadNdjson(gcs, rel, lines) {
|
|
19
|
+
const body = lines.length ? `${lines.join("\n")}\n` : "";
|
|
20
|
+
return gcs.uploadObject(rel, body, { contentType: "application/x-ndjson", resumable: false });
|
|
21
|
+
}
|
|
22
|
+
async function deleteCatalogSnapshotsSubtree(fs, catalogId) {
|
|
23
|
+
const root = fs.collection("catalogSnapshots").doc(String(catalogId));
|
|
24
|
+
const runsSnap = await root.collection("runs").get();
|
|
25
|
+
for (const runDoc of runsSnap.docs) {
|
|
26
|
+
await deleteAllDocsInFirestoreCollection(fs, runDoc.ref.collection("items").path);
|
|
27
|
+
await runDoc.ref.delete();
|
|
28
|
+
}
|
|
29
|
+
await deleteAllDocsInFirestoreCollection(fs, root.collection("items").path);
|
|
30
|
+
await root.delete();
|
|
31
|
+
}
|
|
32
|
+
async function listAllNativeItems(nativeItems, catalogId) {
|
|
33
|
+
const out = [];
|
|
34
|
+
let offset = 0;
|
|
35
|
+
const page = 400;
|
|
36
|
+
while (true) {
|
|
37
|
+
const chunk = await nativeItems.list(catalogId, { limit: page, offset });
|
|
38
|
+
if (!chunk.length)
|
|
39
|
+
break;
|
|
40
|
+
out.push(...chunk);
|
|
41
|
+
offset += chunk.length;
|
|
42
|
+
if (chunk.length < page)
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
async function readJsonArtifact(gcs, rel) {
|
|
48
|
+
if (!rel)
|
|
49
|
+
return null;
|
|
50
|
+
try {
|
|
51
|
+
const buf = await gcs.readObjectBuffer(rel);
|
|
52
|
+
return JSON.parse(buf.toString("utf8"));
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function readNdjsonLines(gcs, rel) {
|
|
59
|
+
const buf = await gcs.readObjectBuffer(rel);
|
|
60
|
+
const text = buf.toString("utf8").trim();
|
|
61
|
+
if (!text)
|
|
62
|
+
return [];
|
|
63
|
+
return text.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
64
|
+
}
|
|
65
|
+
export async function runDeleteCatalog(deps, recordHistory, context, catalogId, input) {
|
|
66
|
+
const issues = [];
|
|
67
|
+
const cid = String(catalogId);
|
|
68
|
+
if (!recordHistory) {
|
|
69
|
+
return {
|
|
70
|
+
ok: false,
|
|
71
|
+
catalogId,
|
|
72
|
+
issues: [
|
|
73
|
+
{
|
|
74
|
+
code: "record_history_required",
|
|
75
|
+
message: "deleteCatalog requires Catalox `recordHistory` (configure `recordHistory` in createCatalox or set CATALOX_RECORD_HISTORY_BUCKET).",
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const cat = await deps.catalogs.get(catalogId);
|
|
81
|
+
if (!cat) {
|
|
82
|
+
return { ok: false, catalogId, issues: [{ code: "catalog_not_found", message: `No catalogs/${cid}` }] };
|
|
83
|
+
}
|
|
84
|
+
if (input.gcsBackupBucket?.trim()) {
|
|
85
|
+
const br = await runBackupData(deps, {
|
|
86
|
+
mode: "gcs",
|
|
87
|
+
gcsBucket: input.gcsBackupBucket.trim(),
|
|
88
|
+
...(input.gcsBackupPrefix != null ? { gcsPrefix: input.gcsBackupPrefix } : {}),
|
|
89
|
+
catalogIds: [catalogId],
|
|
90
|
+
appId: context.appId,
|
|
91
|
+
backupLabel: `pre-delete-${cid}`,
|
|
92
|
+
});
|
|
93
|
+
if (!br.ok) {
|
|
94
|
+
issues.push({ code: "backup_failed", message: br.issues?.[0]?.message ?? "backupData failed" });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const ts = formatBackupTimestamp();
|
|
98
|
+
const folder = `catalogs/${cid}/${ts}__catalog-delete`;
|
|
99
|
+
const gcs = buildRecordHistoryGcsClient(recordHistory);
|
|
100
|
+
const nativeRows = await listAllNativeItems(deps.nativeItems, catalogId);
|
|
101
|
+
const descriptor = await deps.descriptors.get(catalogId);
|
|
102
|
+
const definition = await deps.definitions.get(catalogId);
|
|
103
|
+
const idx = await deps.catalogDataIndex.get(catalogId);
|
|
104
|
+
const bindings = await deps.bindings.listByCatalog(catalogId);
|
|
105
|
+
const refs = await deps.references.listReferencingCatalog(catalogId);
|
|
106
|
+
const snippets = deps.rendererSnippets ? await deps.rendererSnippets.listByCatalogPrefix(catalogId) : [];
|
|
107
|
+
const snapItems = await deps.snapshots.listAllCurrentItems(catalogId);
|
|
108
|
+
const runs = await deps.snapshots.listRuns(catalogId, 500);
|
|
109
|
+
const runLines = [];
|
|
110
|
+
const runItemLines = [];
|
|
111
|
+
for (const r of runs) {
|
|
112
|
+
runLines.push(JSON.stringify(r));
|
|
113
|
+
const items = await deps.snapshots.listRunItems(catalogId, r.runId);
|
|
114
|
+
for (const it of items) {
|
|
115
|
+
runItemLines.push(JSON.stringify({ runId: r.runId, itemId: it.itemId, record: it }));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
let mappingRec = null;
|
|
119
|
+
let adapterRec = null;
|
|
120
|
+
if (definition?.type === "mapped") {
|
|
121
|
+
const md = definition;
|
|
122
|
+
if (md.mappingId)
|
|
123
|
+
mappingRec = (await deps.mappings.get(md.mappingId));
|
|
124
|
+
if (md.adapterId)
|
|
125
|
+
adapterRec = (await deps.adapters.get(md.adapterId));
|
|
126
|
+
}
|
|
127
|
+
const artifacts = {
|
|
128
|
+
catalog: `${folder}/catalog.json`,
|
|
129
|
+
catalogDataIndex: `${folder}/catalogDataIndex.json`,
|
|
130
|
+
nativeItemsNdjson: `${folder}/native-items.ndjson`,
|
|
131
|
+
bindingsNdjson: `${folder}/bindings.ndjson`,
|
|
132
|
+
referencesNdjson: `${folder}/references.ndjson`,
|
|
133
|
+
rendererSnippetsNdjson: `${folder}/renderer-snippets.ndjson`,
|
|
134
|
+
snapshotItemsNdjson: `${folder}/snapshot-items.ndjson`,
|
|
135
|
+
snapshotRunsNdjson: `${folder}/snapshot-runs.ndjson`,
|
|
136
|
+
snapshotRunItemsNdjson: `${folder}/snapshot-run-items.ndjson`,
|
|
137
|
+
};
|
|
138
|
+
if (descriptor) {
|
|
139
|
+
artifacts.descriptor = `${folder}/descriptor.json`;
|
|
140
|
+
await gcsUploadJson(gcs, artifacts.descriptor, descriptor);
|
|
141
|
+
}
|
|
142
|
+
if (definition) {
|
|
143
|
+
artifacts.definition = `${folder}/definition.json`;
|
|
144
|
+
await gcsUploadJson(gcs, artifacts.definition, definition);
|
|
145
|
+
}
|
|
146
|
+
if (mappingRec) {
|
|
147
|
+
artifacts.mapping = `${folder}/mapping.json`;
|
|
148
|
+
await gcsUploadJson(gcs, artifacts.mapping, mappingRec);
|
|
149
|
+
}
|
|
150
|
+
if (adapterRec) {
|
|
151
|
+
artifacts.adapter = `${folder}/adapter.json`;
|
|
152
|
+
await gcsUploadJson(gcs, artifacts.adapter, adapterRec);
|
|
153
|
+
}
|
|
154
|
+
await gcsUploadJson(gcs, artifacts.catalog, cat);
|
|
155
|
+
if (idx)
|
|
156
|
+
await gcsUploadJson(gcs, artifacts.catalogDataIndex, idx);
|
|
157
|
+
await gcsUploadNdjson(gcs, artifacts.nativeItemsNdjson, nativeRows.map((r) => JSON.stringify(r)));
|
|
158
|
+
await gcsUploadNdjson(gcs, artifacts.bindingsNdjson, bindings.map((b) => JSON.stringify(b)));
|
|
159
|
+
await gcsUploadNdjson(gcs, artifacts.referencesNdjson, refs.map((r) => JSON.stringify(r)));
|
|
160
|
+
await gcsUploadNdjson(gcs, artifacts.rendererSnippetsNdjson, snippets.map((s) => JSON.stringify(s)));
|
|
161
|
+
await gcsUploadNdjson(gcs, artifacts.snapshotItemsNdjson, snapItems.map((s) => JSON.stringify(s)));
|
|
162
|
+
await gcsUploadNdjson(gcs, artifacts.snapshotRunsNdjson, runLines);
|
|
163
|
+
await gcsUploadNdjson(gcs, artifacts.snapshotRunItemsNdjson, runItemLines);
|
|
164
|
+
const manifest = {
|
|
165
|
+
schemaVersion: 1,
|
|
166
|
+
kind: "catalox_catalog_delete",
|
|
167
|
+
catalogId: cid,
|
|
168
|
+
deletedAt: new Date().toISOString(),
|
|
169
|
+
artifacts,
|
|
170
|
+
};
|
|
171
|
+
const manifestRel = `${folder}/manifest.json`;
|
|
172
|
+
await gcsUploadJson(gcs, manifestRel, manifest);
|
|
173
|
+
await emitRecordHistoryEventWithCustomPath(recordHistory, context, {
|
|
174
|
+
gcsRelativePath: `${folder}/catalog-delete-event.ndjson`,
|
|
175
|
+
catalogId,
|
|
176
|
+
itemId: "__catalog__",
|
|
177
|
+
op: "catalog_delete_bulk",
|
|
178
|
+
manifest: { manifestRelativePath: manifestRel, folder },
|
|
179
|
+
});
|
|
180
|
+
const fs = deps.firestoreStore.firestore;
|
|
181
|
+
try {
|
|
182
|
+
await deleteAllDocsInFirestoreCollection(fs, nativeItemsCollectionId(catalogId));
|
|
183
|
+
try {
|
|
184
|
+
await deleteAllDocsInFirestoreCollection(fs, fs.collection("catalogData").doc(cid).collection("items").path);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
/* ignore missing legacy */
|
|
188
|
+
}
|
|
189
|
+
await deleteCatalogSnapshotsSubtree(fs, catalogId);
|
|
190
|
+
if (deps.rendererSnippets) {
|
|
191
|
+
for (const s of snippets) {
|
|
192
|
+
const docId = `${String(s.catalogId)}:${String(s.role)}${s.mode ? `:${String(s.mode)}` : ""}`;
|
|
193
|
+
await deps.rendererSnippets.deleteDoc(docId);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
for (const r of refs)
|
|
197
|
+
await deps.references.delete(r.referenceId);
|
|
198
|
+
for (const b of bindings)
|
|
199
|
+
await deps.bindings.delete(b.bindingId);
|
|
200
|
+
if (descriptor)
|
|
201
|
+
await fs.collection("catalogDescriptors").doc(cid).delete();
|
|
202
|
+
if (definition) {
|
|
203
|
+
await fs.collection("catalogDefinitions").doc(cid).delete();
|
|
204
|
+
if (definition.type === "mapped") {
|
|
205
|
+
const md = definition;
|
|
206
|
+
if (md.mappingId)
|
|
207
|
+
await deps.mappings.delete(md.mappingId);
|
|
208
|
+
if (md.adapterId)
|
|
209
|
+
await deps.adapters.delete(md.adapterId);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
await deps.catalogDataIndex.delete(catalogId);
|
|
213
|
+
await deps.catalogs.delete(catalogId);
|
|
214
|
+
}
|
|
215
|
+
catch (e) {
|
|
216
|
+
issues.push({ code: "firestore_delete_failed", message: e instanceof Error ? e.message : String(e) });
|
|
217
|
+
return { ok: false, catalogId, manifestRelativePath: manifestRel, issues };
|
|
218
|
+
}
|
|
219
|
+
deps.nativeItems.invalidateLayoutCache?.(catalogId);
|
|
220
|
+
return { ok: true, catalogId, manifestRelativePath: manifestRel, issues };
|
|
221
|
+
}
|
|
222
|
+
export async function runRestoreDeletedCatalog(deps, recordHistory, input) {
|
|
223
|
+
const issues = [];
|
|
224
|
+
const gcs = buildRecordHistoryGcsClient(recordHistory);
|
|
225
|
+
const rel = input.gcsManifestRelativePath.replace(/^\/+/, "");
|
|
226
|
+
let manifest;
|
|
227
|
+
try {
|
|
228
|
+
const raw = await gcs.readObjectBuffer(rel);
|
|
229
|
+
manifest = JSON.parse(raw.toString("utf8"));
|
|
230
|
+
}
|
|
231
|
+
catch (e) {
|
|
232
|
+
return {
|
|
233
|
+
ok: false,
|
|
234
|
+
catalogId: "",
|
|
235
|
+
issues: [{ code: "manifest_read_failed", message: e instanceof Error ? e.message : String(e) }],
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
const catalogId = manifest.catalogId;
|
|
239
|
+
const cid = String(catalogId);
|
|
240
|
+
try {
|
|
241
|
+
const cat = (await readJsonArtifact(gcs, manifest.artifacts.catalog));
|
|
242
|
+
if (cat)
|
|
243
|
+
await deps.catalogs.upsert(cat);
|
|
244
|
+
const desc = (await readJsonArtifact(gcs, manifest.artifacts.descriptor));
|
|
245
|
+
if (desc)
|
|
246
|
+
await deps.descriptors.upsert(desc);
|
|
247
|
+
const def = (await readJsonArtifact(gcs, manifest.artifacts.definition));
|
|
248
|
+
const map = manifest.artifacts.mapping
|
|
249
|
+
? (await readJsonArtifact(gcs, manifest.artifacts.mapping))
|
|
250
|
+
: null;
|
|
251
|
+
const ad = manifest.artifacts.adapter
|
|
252
|
+
? (await readJsonArtifact(gcs, manifest.artifacts.adapter))
|
|
253
|
+
: null;
|
|
254
|
+
if (ad)
|
|
255
|
+
await deps.adapters.upsert(ad);
|
|
256
|
+
if (map)
|
|
257
|
+
await deps.mappings.upsert(map);
|
|
258
|
+
if (def)
|
|
259
|
+
await deps.definitions.upsert(def);
|
|
260
|
+
const cdi = (await readJsonArtifact(gcs, manifest.artifacts.catalogDataIndex));
|
|
261
|
+
if (cdi)
|
|
262
|
+
await deps.catalogDataIndex.upsert(cdi);
|
|
263
|
+
const nativeLines = await readNdjsonLines(gcs, manifest.artifacts.nativeItemsNdjson);
|
|
264
|
+
for (const line of nativeLines) {
|
|
265
|
+
const rec = JSON.parse(line);
|
|
266
|
+
await deps.nativeItems.upsert(catalogId, rec);
|
|
267
|
+
}
|
|
268
|
+
const bindLines = await readNdjsonLines(gcs, manifest.artifacts.bindingsNdjson);
|
|
269
|
+
for (const line of bindLines) {
|
|
270
|
+
await deps.bindings.upsert(JSON.parse(line));
|
|
271
|
+
}
|
|
272
|
+
const refLines = await readNdjsonLines(gcs, manifest.artifacts.referencesNdjson);
|
|
273
|
+
for (const line of refLines) {
|
|
274
|
+
await deps.references.upsert(JSON.parse(line));
|
|
275
|
+
}
|
|
276
|
+
if (deps.rendererSnippets) {
|
|
277
|
+
const snipLines = await readNdjsonLines(gcs, manifest.artifacts.rendererSnippetsNdjson);
|
|
278
|
+
for (const line of snipLines) {
|
|
279
|
+
await deps.rendererSnippets.upsert(JSON.parse(line));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const fs = deps.firestoreStore.firestore;
|
|
283
|
+
await fs.collection("catalogSnapshots").doc(cid).set({ catalogId: cid, restoredAt: new Date().toISOString() }, { merge: true });
|
|
284
|
+
const runLs = await readNdjsonLines(gcs, manifest.artifacts.snapshotRunsNdjson);
|
|
285
|
+
for (const line of runLs) {
|
|
286
|
+
const run = JSON.parse(line);
|
|
287
|
+
await deps.snapshots.upsertRun(catalogId, run);
|
|
288
|
+
}
|
|
289
|
+
const runItemLs = await readNdjsonLines(gcs, manifest.artifacts.snapshotRunItemsNdjson);
|
|
290
|
+
for (const line of runItemLs) {
|
|
291
|
+
const { runId, record } = JSON.parse(line);
|
|
292
|
+
await deps.snapshots.upsertRunItem(catalogId, runId, record);
|
|
293
|
+
}
|
|
294
|
+
const snapLs = await readNdjsonLines(gcs, manifest.artifacts.snapshotItemsNdjson);
|
|
295
|
+
for (const line of snapLs) {
|
|
296
|
+
const rec = JSON.parse(line);
|
|
297
|
+
await deps.snapshots.upsert(catalogId, rec);
|
|
298
|
+
}
|
|
299
|
+
deps.nativeItems.invalidateLayoutCache?.(catalogId);
|
|
300
|
+
return { ok: true, catalogId, issues };
|
|
301
|
+
}
|
|
302
|
+
catch (e) {
|
|
303
|
+
issues.push({ code: "restore_failed", message: e instanceof Error ? e.message : String(e) });
|
|
304
|
+
return { ok: false, catalogId, issues };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
export async function runRenameCatalog(deps, recordHistory, context, fromCatalogId, toCatalogId, input) {
|
|
308
|
+
const issues = [];
|
|
309
|
+
const from = String(fromCatalogId);
|
|
310
|
+
const to = String(toCatalogId);
|
|
311
|
+
if (!recordHistory) {
|
|
312
|
+
return {
|
|
313
|
+
ok: false,
|
|
314
|
+
fromCatalogId,
|
|
315
|
+
toCatalogId,
|
|
316
|
+
issues: [
|
|
317
|
+
{
|
|
318
|
+
code: "record_history_required",
|
|
319
|
+
message: "renameCatalog requires Catalox `recordHistory` (configure `recordHistory` in createCatalox or set CATALOX_RECORD_HISTORY_BUCKET).",
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
if (from === to) {
|
|
325
|
+
return { ok: false, fromCatalogId, toCatalogId, issues: [{ code: "same_id", message: "from and to catalog ids must differ" }] };
|
|
326
|
+
}
|
|
327
|
+
const existingTo = await deps.catalogs.get(toCatalogId);
|
|
328
|
+
if (existingTo) {
|
|
329
|
+
return {
|
|
330
|
+
ok: false,
|
|
331
|
+
fromCatalogId,
|
|
332
|
+
toCatalogId,
|
|
333
|
+
issues: [{ code: "target_exists", message: `catalogs/${to} already exists` }],
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
const srcCat = await deps.catalogs.get(fromCatalogId);
|
|
337
|
+
if (!srcCat) {
|
|
338
|
+
return { ok: false, fromCatalogId, toCatalogId, issues: [{ code: "source_missing", message: `catalogs/${from} not found` }] };
|
|
339
|
+
}
|
|
340
|
+
if (input.gcsBackupBucket?.trim()) {
|
|
341
|
+
await runBackupData(deps, {
|
|
342
|
+
mode: "gcs",
|
|
343
|
+
gcsBucket: input.gcsBackupBucket.trim(),
|
|
344
|
+
...(input.gcsBackupPrefix != null ? { gcsPrefix: input.gcsBackupPrefix } : {}),
|
|
345
|
+
catalogIds: [fromCatalogId],
|
|
346
|
+
appId: context.appId,
|
|
347
|
+
backupLabel: `pre-rename-${from}-to-${to}`,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
const ts = formatBackupTimestamp();
|
|
351
|
+
const folder = `catalogs/${from}__to__${to}/${ts}__catalog-rename`;
|
|
352
|
+
const gcs = buildRecordHistoryGcsClient(recordHistory);
|
|
353
|
+
const nativeCount = (await listAllNativeItems(deps.nativeItems, fromCatalogId)).length;
|
|
354
|
+
const manifest = {
|
|
355
|
+
schemaVersion: 1,
|
|
356
|
+
kind: "catalox_catalog_rename",
|
|
357
|
+
fromCatalogId: from,
|
|
358
|
+
toCatalogId: to,
|
|
359
|
+
renamedAt: new Date().toISOString(),
|
|
360
|
+
folder,
|
|
361
|
+
nativeItemCount: nativeCount,
|
|
362
|
+
};
|
|
363
|
+
const manifestRel = `${folder}/manifest.json`;
|
|
364
|
+
await gcsUploadJson(gcs, manifestRel, manifest);
|
|
365
|
+
await emitRecordHistoryEventWithCustomPath(recordHistory, context, {
|
|
366
|
+
gcsRelativePath: `${folder}/catalog-rename-event.ndjson`,
|
|
367
|
+
catalogId: toCatalogId,
|
|
368
|
+
itemId: "__catalog__",
|
|
369
|
+
op: "catalog_rename",
|
|
370
|
+
manifest,
|
|
371
|
+
});
|
|
372
|
+
const fs = deps.firestoreStore.firestore;
|
|
373
|
+
try {
|
|
374
|
+
const nextCat = { ...srcCat, catalogId: toCatalogId, updatedAt: new Date().toISOString() };
|
|
375
|
+
await deps.catalogs.upsert(nextCat);
|
|
376
|
+
const desc = await deps.descriptors.get(fromCatalogId);
|
|
377
|
+
if (desc) {
|
|
378
|
+
const next = {
|
|
379
|
+
...desc,
|
|
380
|
+
catalogId: toCatalogId,
|
|
381
|
+
descriptor: { ...desc.descriptor, catalogId: toCatalogId },
|
|
382
|
+
updatedAt: new Date().toISOString(),
|
|
383
|
+
};
|
|
384
|
+
await deps.descriptors.upsert(next);
|
|
385
|
+
await fs.collection("catalogDescriptors").doc(from).delete();
|
|
386
|
+
}
|
|
387
|
+
const def = await deps.definitions.get(fromCatalogId);
|
|
388
|
+
if (def) {
|
|
389
|
+
let nextDef = {
|
|
390
|
+
...def,
|
|
391
|
+
catalogId: toCatalogId,
|
|
392
|
+
updatedAt: new Date().toISOString(),
|
|
393
|
+
};
|
|
394
|
+
if (def.type === "native") {
|
|
395
|
+
const n = def;
|
|
396
|
+
nextDef = {
|
|
397
|
+
...n,
|
|
398
|
+
catalogId: toCatalogId,
|
|
399
|
+
storage: { ...n.storage, collectionPath: nativeItemsCollectionId(toCatalogId) },
|
|
400
|
+
updatedAt: new Date().toISOString(),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
await deps.definitions.upsert(nextDef);
|
|
404
|
+
await fs.collection("catalogDefinitions").doc(from).delete();
|
|
405
|
+
}
|
|
406
|
+
const cdi = await deps.catalogDataIndex.get(fromCatalogId);
|
|
407
|
+
if (cdi) {
|
|
408
|
+
await deps.catalogDataIndex.upsert({ ...cdi, catalogId: toCatalogId, updatedAt: new Date().toISOString() });
|
|
409
|
+
await deps.catalogDataIndex.delete(fromCatalogId);
|
|
410
|
+
}
|
|
411
|
+
const rows = await listAllNativeItems(deps.nativeItems, fromCatalogId);
|
|
412
|
+
for (const r of rows) {
|
|
413
|
+
const next = { ...r, catalogId: toCatalogId };
|
|
414
|
+
await deps.nativeItems.upsert(toCatalogId, next);
|
|
415
|
+
}
|
|
416
|
+
await deleteAllDocsInFirestoreCollection(fs, nativeItemsCollectionId(fromCatalogId));
|
|
417
|
+
try {
|
|
418
|
+
await deleteAllDocsInFirestoreCollection(fs, fs.collection("catalogData").doc(from).collection("items").path);
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
/* ignore */
|
|
422
|
+
}
|
|
423
|
+
const runs = await deps.snapshots.listRuns(fromCatalogId, 500);
|
|
424
|
+
for (const run of runs) {
|
|
425
|
+
await deps.snapshots.upsertRun(toCatalogId, { ...run, catalogId: toCatalogId });
|
|
426
|
+
const items = await deps.snapshots.listRunItems(fromCatalogId, run.runId);
|
|
427
|
+
for (const it of items) {
|
|
428
|
+
await deps.snapshots.upsertRunItem(toCatalogId, run.runId, { ...it, catalogId: toCatalogId });
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const snapItems = await deps.snapshots.listAllCurrentItems(fromCatalogId);
|
|
432
|
+
for (const s of snapItems) {
|
|
433
|
+
await deps.snapshots.upsert(toCatalogId, { ...s, catalogId: toCatalogId });
|
|
434
|
+
}
|
|
435
|
+
await deleteCatalogSnapshotsSubtree(fs, fromCatalogId);
|
|
436
|
+
if (deps.rendererSnippets) {
|
|
437
|
+
const snippets = await deps.rendererSnippets.listByCatalogPrefix(fromCatalogId);
|
|
438
|
+
for (const s of snippets) {
|
|
439
|
+
const oldId = `${String(s.catalogId)}:${String(s.role)}${s.mode ? `:${String(s.mode)}` : ""}`;
|
|
440
|
+
const next = { ...s, catalogId: toCatalogId };
|
|
441
|
+
await deps.rendererSnippets.upsert(next);
|
|
442
|
+
await deps.rendererSnippets.deleteDoc(oldId);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const refs = await deps.references.listReferencingCatalog(fromCatalogId);
|
|
446
|
+
for (const r of refs) {
|
|
447
|
+
const fc = r.fromCatalogId === from ? to : String(r.fromCatalogId);
|
|
448
|
+
const tc = r.toCatalogId === from ? to : String(r.toCatalogId);
|
|
449
|
+
const nextRefId = `${fc}:${r.fromItemId}:${r.relationType}:${tc}:${r.toItemId}`;
|
|
450
|
+
await deps.references.delete(r.referenceId);
|
|
451
|
+
await deps.references.upsert({
|
|
452
|
+
...r,
|
|
453
|
+
referenceId: nextRefId,
|
|
454
|
+
fromCatalogId: fc,
|
|
455
|
+
toCatalogId: tc,
|
|
456
|
+
updatedAt: new Date().toISOString(),
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
const binds = await deps.bindings.listByCatalog(fromCatalogId);
|
|
460
|
+
for (const b of binds) {
|
|
461
|
+
await deps.bindings.delete(b.bindingId);
|
|
462
|
+
const nb = {
|
|
463
|
+
...b,
|
|
464
|
+
catalogId: toCatalogId,
|
|
465
|
+
bindingId: `${String(b.appId)}:${to}`,
|
|
466
|
+
updatedAt: new Date().toISOString(),
|
|
467
|
+
};
|
|
468
|
+
await deps.bindings.upsert(nb);
|
|
469
|
+
}
|
|
470
|
+
await deps.catalogs.delete(fromCatalogId);
|
|
471
|
+
deps.nativeItems.invalidateLayoutCache?.(fromCatalogId);
|
|
472
|
+
deps.nativeItems.invalidateLayoutCache?.(toCatalogId);
|
|
473
|
+
return { ok: true, fromCatalogId, toCatalogId, manifestRelativePath: manifestRel, issues };
|
|
474
|
+
}
|
|
475
|
+
catch (e) {
|
|
476
|
+
issues.push({ code: "rename_failed", message: e instanceof Error ? e.message : String(e) });
|
|
477
|
+
return { ok: false, fromCatalogId, toCatalogId, manifestRelativePath: manifestRel, issues };
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
//# sourceMappingURL=catalog-lifecycle.js.map
|