refactor(8.0): rename BackupData → PortableGraph (the type is interchange, not a backup)
The export()/import() document type was named BackupData, but it is not a backup: it is the portable, versioned, partial-or-whole interchange representation of a graph (entities + relations + optional vectors/blobs) — the unit Brainy exports for transport between instances, versions, and products, and the payload other artifacts embed. The actual backup is persist()/load() (the native whole-brain, generation-preserving snapshot), so "Backup*" actively collided with that concept. Rename every developer-visible symbol, file, doc, JSDoc and comment: - BackupData→PortableGraph, BackupEntity→PortableGraphEntity, BackupRelation→PortableGraphRelation, BackupReader/Writer→PortableGraphReader/Writer, BackupValidation→PortableGraphValidation, isBackupData→isPortableGraph, validateBackup→validatePortableGraph, BACKUP_FORMAT[_VERSION]→PORTABLE_GRAPH_FORMAT[_VERSION]. - src/db/backup.ts → src/db/portableGraph.ts; test → db-portable-graph.test.ts. - Guide, api/README, RELEASES updated. The on-the-wire `format` tag is renamed 'brainy-backup' → 'brainy-portable-graph': the format was introduced in 7.32.0 and has not been adopted by any consumer, so there are no stored documents to stay compatible with — a clean rename beats carrying a legacy tag. No deprecated aliases (nothing to alias). 7.x shipped the same rename as 7.32.2. 1471 unit green; build clean; db-portable-graph.test.ts 17/17.
This commit is contained in:
parent
3a3aa43b3a
commit
373a48122d
8 changed files with 156 additions and 145 deletions
21
RELEASES.md
21
RELEASES.md
|
|
@ -80,7 +80,7 @@ historical records) are documented in:
|
||||||
- [docs/ADR-001-generational-mvcc.md](docs/ADR-001-generational-mvcc.md) — the
|
- [docs/ADR-001-generational-mvcc.md](docs/ADR-001-generational-mvcc.md) — the
|
||||||
design record: persisted layout, commit protocol, crash recovery, proof table
|
design record: persisted layout, commit protocol, crash recovery, proof table
|
||||||
|
|
||||||
### Portable export & import (BackupData v1)
|
### Portable export & import (PortableGraph v1)
|
||||||
|
|
||||||
A portable, versioned graph format that serializes part or all of a brain to a
|
A portable, versioned graph format that serializes part or all of a brain to a
|
||||||
single JSON document and restores it — the cross-environment, cross-version
|
single JSON document and restores it — the cross-environment, cross-version
|
||||||
|
|
@ -88,29 +88,34 @@ single JSON document and restores it — the cross-environment, cross-version
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// Export is a method on the immutable Db, so it composes with now()/asOf()/with()
|
// Export is a method on the immutable Db, so it composes with now()/asOf()/with()
|
||||||
const backup = await brain.export({ collection: id }, { includeVectors: true })
|
const graph = await brain.export({ collection: id }, { includeVectors: true })
|
||||||
await otherBrain.import(backup, { onConflict: 'merge' }) // dedup-by-id
|
await otherBrain.import(graph, { onConflict: 'merge' }) // dedup-by-id
|
||||||
|
|
||||||
;(await brain.asOf(gen)).export(sel) // time-travel export
|
;(await brain.asOf(gen)).export(sel) // time-travel export
|
||||||
brain.now().with(ops).export(sel) // what-if export
|
brain.now().with(ops).export(sel) // what-if export
|
||||||
```
|
```
|
||||||
|
|
||||||
- **`brain.export(selector?, options?)` / `db.export(...)`** → a versioned
|
- **`brain.export(selector?, options?)` / `db.export(...)`** → a versioned
|
||||||
`BackupData` document. Selectors reuse `find()`'s grammar: `{ ids }`,
|
`PortableGraph` document. Selectors reuse `find()`'s grammar: `{ ids }`,
|
||||||
`{ collection }` (alias `memberOf`, transitive `Contains`),
|
`{ collection }` (alias `memberOf`, transitive `Contains`),
|
||||||
`{ connected: { from, depth } }`, `{ vfsPath }`, predicate
|
`{ connected: { from, depth } }`, `{ vfsPath }`, predicate
|
||||||
(`{ type, subtype, where, service }`), or the whole brain (omit) — and they
|
(`{ type, subtype, where, service }`), or the whole brain (omit) — and they
|
||||||
compose. Options: `includeVectors`, `includeContent` (VFS file bytes),
|
compose. Options: `includeVectors`, `includeContent` (VFS file bytes),
|
||||||
`includeSystem`, `edges: 'induced' | 'incident' | 'none'`.
|
`includeSystem`, `edges: 'induced' | 'incident' | 'none'`.
|
||||||
- **`brain.import(backup, options?)`** is polymorphic: a `BackupData` document is
|
- **`brain.import(graph, options?)`** is polymorphic: a `PortableGraph` document is
|
||||||
restored as **one atomic transaction** (`onConflict: 'merge' | 'replace' | 'skip'`,
|
restored as **one atomic transaction** (`onConflict: 'merge' | 'replace' | 'skip'`,
|
||||||
`reembed: 'auto' | 'never'`, `remapIds` for cloning); a file/buffer routes to the
|
`reembed: 'auto' | 'never'`, `remapIds` for cloning); a file/buffer routes to the
|
||||||
existing CSV/PDF/Excel/JSON ingestion. No migration for ingestion callers.
|
existing CSV/PDF/Excel/JSON ingestion. No migration for ingestion callers.
|
||||||
- **`validateBackup(data)`** — dry-run structural/version/endpoint check before import.
|
- **`validatePortableGraph(data)`** — dry-run structural/version/endpoint check before import.
|
||||||
- **Format** (`format:'brainy-backup'`, `formatVersion: 1`) is identical on 7.x
|
- **Format** (`format:'brainy-portable-graph'`, `formatVersion: 1`) is identical on
|
||||||
(7.32.0) and 8.0; standard fields (`subtype`/`visibility`/`data`/…) are top-level,
|
7.x and 8.0; standard fields (`subtype`/`visibility`/`data`/…) are top-level,
|
||||||
`metadata` is custom-only. Current-state (no generation history — that lives in
|
`metadata` is custom-only. Current-state (no generation history — that lives in
|
||||||
`persist()`). Types exported from the package root.
|
`persist()`). Types exported from the package root.
|
||||||
|
- **Naming:** the type is `PortableGraph` (with `PortableGraphEntity` /
|
||||||
|
`PortableGraphRelation`) — it is the portable interchange form of a graph, not a
|
||||||
|
backup (that role is `persist()`/`load()`). Renamed from the short-lived
|
||||||
|
`BackupData`/`'brainy-backup'` (introduced in 7.32.0, never adopted) with no
|
||||||
|
compatibility shim.
|
||||||
- Guide: [docs/guides/export-and-import.md](docs/guides/export-and-import.md).
|
- Guide: [docs/guides/export-and-import.md](docs/guides/export-and-import.md).
|
||||||
|
|
||||||
### Aggregation: distinctCount over any value type
|
### Aggregation: distinctCount over any value type
|
||||||
|
|
|
||||||
|
|
@ -1415,16 +1415,16 @@ await brain.import('https://api.example.com/data.json')
|
||||||
|
|
||||||
### Export & Import (portable) + Snapshots (native)
|
### Export & Import (portable) + Snapshots (native)
|
||||||
|
|
||||||
**Portable graph backup** — `brain.export()` / `brain.import()` (`BackupData` v1, versioned
|
**Portable graph export/import** — `brain.export()` / `brain.import()` (`PortableGraph` v1, versioned
|
||||||
JSON, partial-or-whole, cross-version). `export()` lives on the immutable `Db`, so it composes
|
JSON, partial-or-whole, cross-version). `export()` lives on the immutable `Db`, so it composes
|
||||||
with `now()`/`asOf()`/`with()`:
|
with `now()`/`asOf()`/`with()`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Export part or all of the brain to a portable, versioned document
|
// Export part or all of the brain to a portable, versioned document
|
||||||
const backup = await brain.export({ ids }, { includeVectors: true })
|
const graph = await brain.export({ ids }, { includeVectors: true })
|
||||||
|
|
||||||
// Restore it — import() routes a BackupData to the graph round-trip (merge by id)
|
// Restore it — import() routes a PortableGraph to the graph round-trip (merge by id)
|
||||||
await otherBrain.import(backup, { onConflict: 'merge' })
|
await otherBrain.import(graph, { onConflict: 'merge' })
|
||||||
|
|
||||||
// Time-travel export (serialize a past generation) / what-if export (a speculative state)
|
// Time-travel export (serialize a past generation) / what-if export (a speculative state)
|
||||||
const past = await brain.asOf(gen)
|
const past = await brain.asOf(gen)
|
||||||
|
|
@ -1436,7 +1436,7 @@ await brain.now().with(ops).export({ ids })
|
||||||
Selectors: `{ ids }`, `{ collection }` (alias `memberOf`), `{ connected: { from, depth } }`,
|
Selectors: `{ ids }`, `{ collection }` (alias `memberOf`), `{ connected: { from, depth } }`,
|
||||||
`{ vfsPath }`, predicate (`{ type, subtype, where, service }`), or whole brain (omit). See the
|
`{ vfsPath }`, predicate (`{ type, subtype, where, service }`), or whole brain (omit). See the
|
||||||
**[Export & Import guide](../guides/export-and-import.md)**. Distinct from `brain.import(file)`
|
**[Export & Import guide](../guides/export-and-import.md)**. Distinct from `brain.import(file)`
|
||||||
(CSV/PDF/Excel/JSON ingestion — `import()` dispatches on whether you pass a `BackupData` or a file).
|
(CSV/PDF/Excel/JSON ingestion — `import()` dispatches on whether you pass a `PortableGraph` or a file).
|
||||||
|
|
||||||
**Native whole-brain snapshot** (generation-preserving, not portable JSON):
|
**Native whole-brain snapshot** (generation-preserving, not portable JSON):
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ public: true
|
||||||
category: guides
|
category: guides
|
||||||
template: guide
|
template: guide
|
||||||
order: 9
|
order: 9
|
||||||
description: Export part or all of a brain to a portable, versioned BackupData document and import it back — by id, collection, connected neighbourhood, VFS subtree, predicate, or the whole brain. export() lives on the immutable Db, so asOf()/with() give time-travel and what-if exports.
|
description: Export part or all of a brain to a portable, versioned PortableGraph document and import it back — by id, collection, connected neighbourhood, VFS subtree, predicate, or the whole brain. export() lives on the immutable Db, so asOf()/with() give time-travel and what-if exports.
|
||||||
next:
|
next:
|
||||||
- guides/subtypes-and-facets
|
- guides/subtypes-and-facets
|
||||||
- api/README
|
- api/README
|
||||||
|
|
@ -15,16 +15,16 @@ next:
|
||||||
|
|
||||||
Brainy serializes part or all of a brain — an item, a collection, a connected
|
Brainy serializes part or all of a brain — an item, a collection, a connected
|
||||||
neighbourhood, a VFS subtree, a predicate match, or the whole brain — into a single
|
neighbourhood, a VFS subtree, a predicate match, or the whole brain — into a single
|
||||||
versioned JSON document (`BackupData`), and restores it.
|
versioned JSON document (`PortableGraph`), and restores it.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const backup = await brain.export() // whole brain → BackupData
|
const graph = await brain.export() // whole brain → PortableGraph
|
||||||
await brain.import(backup) // restore (merge by id, re-embed if no vectors)
|
await brain.import(graph) // restore (merge by id, re-embed if no vectors)
|
||||||
```
|
```
|
||||||
|
|
||||||
It is **portable** (human-readable JSON), **versioned** (`formatVersion`, so a document
|
It is **portable** (human-readable JSON), **versioned** (`formatVersion`, so a document
|
||||||
written by 7.x imports cleanly into 8.0), and **current-state** (the entities and edges as
|
written by 7.x imports cleanly into 8.0), and **current-state** (the entities and edges as
|
||||||
they are now — no generation history). Use it for portable artifacts, partial backups,
|
they are now — no generation history). Use it for portable artifacts, partial exports,
|
||||||
cross-environment moves, and version upgrades.
|
cross-environment moves, and version upgrades.
|
||||||
|
|
||||||
`export()` is a method on the **immutable `Db` value**, so it composes with every way of
|
`export()` is a method on the **immutable `Db` value**, so it composes with every way of
|
||||||
|
|
@ -44,14 +44,14 @@ brain.now().with(ops).export(sel) // what-if export (a specula
|
||||||
| A whole-brain snapshot **with generation history** | `brain.now().persist(path)` / `Brainy.load(path)` (native) |
|
| A whole-brain snapshot **with generation history** | `brain.now().persist(path)` / `Brainy.load(path)` (native) |
|
||||||
| To ingest a CSV / PDF / Excel / JSON **file** as new entities | `brain.import(file)` — see [Import Anything](./import-anything.md) |
|
| To ingest a CSV / PDF / Excel / JSON **file** as new entities | `brain.import(file)` — see [Import Anything](./import-anything.md) |
|
||||||
|
|
||||||
`import()` is **polymorphic**: hand it a `BackupData` and it does the graph round-trip;
|
`import()` is **polymorphic**: hand it a `PortableGraph` and it does the graph round-trip;
|
||||||
hand it a file/buffer and it does foreign-file ingestion (dispatched on the document's
|
hand it a file/buffer and it does foreign-file ingestion (dispatched on the document's
|
||||||
`format: 'brainy-backup'` tag).
|
`format: 'brainy-portable-graph'` tag).
|
||||||
|
|
||||||
## Exporting
|
## Exporting
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
brain.export(selector?, options?): Promise<BackupData>
|
brain.export(selector?, options?): Promise<PortableGraph>
|
||||||
// (also on any Db: brain.now().export(...), (await brain.asOf(g)).export(...))
|
// (also on any Db: brain.now().export(...), (await brain.asOf(g)).export(...))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -92,20 +92,20 @@ await brain.export({ ids: hits.map(r => r.id) })
|
||||||
## Importing
|
## Importing
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
brain.import(backup, options?): Promise<ImportResult>
|
brain.import(graph, options?): Promise<ImportResult>
|
||||||
```
|
```
|
||||||
|
|
||||||
The whole backup is applied as **one atomic transaction** — it advances the brain exactly
|
The whole graph is applied as **one atomic transaction** — it advances the brain exactly
|
||||||
one generation, or none on failure.
|
one generation, or none on failure.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const result = await brain.import(backup, { onConflict: 'merge' })
|
const result = await brain.import(graph, { onConflict: 'merge' })
|
||||||
// → { imported, merged, skipped, reembedded, blobsWritten, errors }
|
// → { imported, merged, skipped, reembedded, blobsWritten, errors }
|
||||||
```
|
```
|
||||||
|
|
||||||
| Option | Default | Effect |
|
| Option | Default | Effect |
|
||||||
|--------|---------|--------|
|
|--------|---------|--------|
|
||||||
| `onConflict` | `'merge'` | `'merge'` (update existing id in place — assemble many backups), `'replace'` (delete + recreate), or `'skip'`. |
|
| `onConflict` | `'merge'` | `'merge'` (update existing id in place — assemble many exported graphs), `'replace'` (delete + recreate), or `'skip'`. |
|
||||||
| `reembed` | `'auto'` | `'auto'` (use the carried vector, else re-embed from `data`) or `'never'` (require a carried vector; record an error if absent). |
|
| `reembed` | `'auto'` | `'auto'` (use the carried vector, else re-embed from `data`) or `'never'` (require a carried vector; record an error if absent). |
|
||||||
| `remapIds` | — | Rewrite every id on the way in, e.g. to clone a template subgraph under fresh ids. |
|
| `remapIds` | — | Rewrite every id on the way in, e.g. to clone a template subgraph under fresh ids. |
|
||||||
| `meta` | — | Transaction metadata recorded in the tx-log alongside the new generation. |
|
| `meta` | — | Transaction metadata recorded in the tx-log alongside the new generation. |
|
||||||
|
|
@ -113,11 +113,11 @@ const result = await brain.import(backup, { onConflict: 'merge' })
|
||||||
The default `onConflict: 'merge'` lets you assemble one working graph from many exported
|
The default `onConflict: 'merge'` lets you assemble one working graph from many exported
|
||||||
documents that share entity ids — re-importing an id merges rather than duplicates.
|
documents that share entity ids — re-importing an id merges rather than duplicates.
|
||||||
|
|
||||||
## The `BackupData` format
|
## The `PortableGraph` format
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
{
|
{
|
||||||
"format": "brainy-backup",
|
"format": "brainy-portable-graph", // identifies the document type
|
||||||
"formatVersion": 1, // import gates on this (cross-version migration)
|
"formatVersion": 1, // import gates on this (cross-version migration)
|
||||||
"brainyVersion": "8.0.0",
|
"brainyVersion": "8.0.0",
|
||||||
"createdAt": "2026-06-16T…Z",
|
"createdAt": "2026-06-16T…Z",
|
||||||
|
|
@ -145,7 +145,7 @@ documents that share entity ids — re-importing an id merges rather than duplic
|
||||||
Standard fields (`subtype`, `visibility`, `data`, `confidence`, `weight`, `service`) sit at
|
Standard fields (`subtype`, `visibility`, `data`, `confidence`, `weight`, `service`) sit at
|
||||||
the top level of each entity; `metadata` holds **only** custom user fields — mirroring the
|
the top level of each entity; `metadata` holds **only** custom user fields — mirroring the
|
||||||
in-memory `Entity` shape, so `import()` maps each field to its dedicated parameter. The
|
in-memory `Entity` shape, so `import()` maps each field to its dedicated parameter. The
|
||||||
TypeScript types (`BackupData`, `BackupEntity`, `BackupRelation`, `ExportSelector`,
|
TypeScript types (`PortableGraph`, `PortableGraphEntity`, `PortableGraphRelation`, `ExportSelector`,
|
||||||
`ExportOptions`, `ImportOptions`, `ImportResult`) are exported from the package root.
|
`ExportOptions`, `ImportOptions`, `ImportResult`) are exported from the package root.
|
||||||
|
|
||||||
## Generations & time-travel
|
## Generations & time-travel
|
||||||
|
|
@ -164,7 +164,7 @@ generation, so time-travel export differs across transaction boundaries.
|
||||||
|
|
||||||
## Cross-version (7.x → 8.0)
|
## Cross-version (7.x → 8.0)
|
||||||
|
|
||||||
Because the document is shared and versioned, a backup written by 7.x imports into 8.0:
|
Because the document is shared and versioned, a PortableGraph written by 7.x imports into 8.0:
|
||||||
`formatVersion` is read forward, `subtype` is carried so 8.0 re-types correctly, and the
|
`formatVersion` is read forward, `subtype` is carried so 8.0 re-types correctly, and the
|
||||||
same 384-dimension model on both lines means `includeVectors:false` re-embeds identically
|
same 384-dimension model on both lines means `includeVectors:false` re-embeds identically
|
||||||
(or `true` carries vectors verbatim).
|
(or `true` carries vectors verbatim).
|
||||||
|
|
|
||||||
|
|
@ -117,13 +117,13 @@ import * as fs from 'node:fs'
|
||||||
import { Db, type DbHost, type HistoricalQueryHandle } from './db/db.js'
|
import { Db, type DbHost, type HistoricalQueryHandle } from './db/db.js'
|
||||||
import {
|
import {
|
||||||
importGraph,
|
importGraph,
|
||||||
isBackupData,
|
isPortableGraph,
|
||||||
type BackupData,
|
type PortableGraph,
|
||||||
type ExportSelector,
|
type ExportSelector,
|
||||||
type ExportOptions,
|
type ExportOptions,
|
||||||
type ImportOptions,
|
type ImportOptions,
|
||||||
type ImportResult
|
type ImportResult
|
||||||
} from './db/backup.js'
|
} from './db/portableGraph.js'
|
||||||
import { GenerationStore } from './db/generationStore.js'
|
import { GenerationStore } from './db/generationStore.js'
|
||||||
import { isDeterministicEmbedMode } from './embeddings/deterministicEmbedMode.js'
|
import { isDeterministicEmbedMode } from './embeddings/deterministicEmbedMode.js'
|
||||||
import { GenerationConflictError } from './db/errors.js'
|
import { GenerationConflictError } from './db/errors.js'
|
||||||
|
|
@ -5074,22 +5074,22 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Serialize part or all of the brain into a portable `BackupData`
|
* @description Serialize part or all of the brain into a portable `PortableGraph`
|
||||||
* document — sugar for `brain.now().export(selector, options)` (the current
|
* document — sugar for `brain.now().export(selector, options)` (the current
|
||||||
* generation). For a past generation use `(await brain.asOf(g)).export(...)`;
|
* generation). For a past generation use `(await brain.asOf(g)).export(...)`;
|
||||||
* for a speculative state use `brain.now().with(ops).export(...)`.
|
* for a speculative state use `brain.now().with(ops).export(...)`.
|
||||||
*
|
*
|
||||||
* Restore a `BackupData` with {@link Brainy.import} (it dispatches a backup to the
|
* Restore a `PortableGraph` with {@link Brainy.import} (it dispatches a backup to the
|
||||||
* graph round-trip; a file/buffer to ingestion). Distinct from `db.persist()`
|
* graph round-trip; a file/buffer to ingestion). Distinct from `db.persist()`
|
||||||
* (native whole-brain snapshot with generation history).
|
* (native whole-brain snapshot with generation history).
|
||||||
*
|
*
|
||||||
* @param selector - WHAT to export (omit for the whole brain). See {@link ExportSelector}.
|
* @param selector - WHAT to export (omit for the whole brain). See {@link ExportSelector}.
|
||||||
* @param options - HOW to export (vectors / VFS bytes / edge policy). See {@link ExportOptions}.
|
* @param options - HOW to export (vectors / VFS bytes / edge policy). See {@link ExportOptions}.
|
||||||
* @returns A versioned, portable `BackupData` document.
|
* @returns A versioned, portable `PortableGraph` document.
|
||||||
* @example
|
* @example
|
||||||
* const backup = await brain.export({ ids }, { includeVectors: true })
|
* const backup = await brain.export({ ids }, { includeVectors: true })
|
||||||
*/
|
*/
|
||||||
async export(selector: ExportSelector = {}, options: ExportOptions = {}): Promise<BackupData> {
|
async export(selector: ExportSelector = {}, options: ExportOptions = {}): Promise<PortableGraph> {
|
||||||
return this.now().export(selector, options)
|
return this.now().export(selector, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -6743,7 +6743,7 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
||||||
* - Reduced confusion (removed redundant options)
|
* - Reduced confusion (removed redundant options)
|
||||||
*/
|
*/
|
||||||
async import(
|
async import(
|
||||||
source: Buffer | string | object | BackupData,
|
source: Buffer | string | object | PortableGraph,
|
||||||
options?: {
|
options?: {
|
||||||
format?: 'excel' | 'pdf' | 'csv' | 'json' | 'markdown' | 'yaml' | 'docx' | 'image'
|
format?: 'excel' | 'pdf' | 'csv' | 'json' | 'markdown' | 'yaml' | 'docx' | 'image'
|
||||||
vfsPath?: string
|
vfsPath?: string
|
||||||
|
|
@ -6770,9 +6770,9 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
||||||
}) => void
|
}) => void
|
||||||
} & Partial<ImportOptions>
|
} & Partial<ImportOptions>
|
||||||
): Promise<ImportResult | any> {
|
): Promise<ImportResult | any> {
|
||||||
// Portable graph round-trip: a BackupData document (produced by export()) is
|
// Portable graph round-trip: a PortableGraph document (produced by export()) is
|
||||||
// restored via ONE atomic transaction — distinct from foreign-file ingestion below.
|
// restored via ONE atomic transaction — distinct from foreign-file ingestion below.
|
||||||
if (isBackupData(source)) {
|
if (isPortableGraph(source)) {
|
||||||
return importGraph(this, this.storage, source, options as ImportOptions)
|
return importGraph(this, this.storage, source, options as ImportOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
10
src/db/db.ts
10
src/db/db.ts
|
|
@ -57,8 +57,8 @@ import type {
|
||||||
Result
|
Result
|
||||||
} from '../types/brainy.types.js'
|
} from '../types/brainy.types.js'
|
||||||
import type { StorageAdapter } from '../coreTypes.js'
|
import type { StorageAdapter } from '../coreTypes.js'
|
||||||
import { exportGraph } from './backup.js'
|
import { exportGraph } from './portableGraph.js'
|
||||||
import type { ExportSelector, ExportOptions, BackupData } from './backup.js'
|
import type { ExportSelector, ExportOptions, PortableGraph } from './portableGraph.js'
|
||||||
import {
|
import {
|
||||||
splitNounMetadataRecord,
|
splitNounMetadataRecord,
|
||||||
splitVerbMetadataRecord
|
splitVerbMetadataRecord
|
||||||
|
|
@ -416,7 +416,7 @@ export class Db<T = any> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Serialize part or all of this database value into a portable
|
* @description Serialize part or all of this database value into a portable
|
||||||
* `BackupData` document, read **as of this view's generation**. Because `export`
|
* `PortableGraph` document, read **as of this view's generation**. Because `export`
|
||||||
* lives on the immutable `Db`, it composes with every way of obtaining one:
|
* lives on the immutable `Db`, it composes with every way of obtaining one:
|
||||||
* `brain.now().export(sel)` (current), `(await brain.asOf(g)).export(sel)`
|
* `brain.now().export(sel)` (current), `(await brain.asOf(g)).export(sel)`
|
||||||
* (time-travel export), and `brain.now().with(ops).export(sel)` (what-if export).
|
* (time-travel export), and `brain.now().with(ops).export(sel)` (what-if export).
|
||||||
|
|
@ -427,11 +427,11 @@ export class Db<T = any> {
|
||||||
*
|
*
|
||||||
* @param selector - WHAT to export (omit for the whole brain). See {@link ExportSelector}.
|
* @param selector - WHAT to export (omit for the whole brain). See {@link ExportSelector}.
|
||||||
* @param options - HOW to export (vectors / VFS bytes / edge policy). See {@link ExportOptions}.
|
* @param options - HOW to export (vectors / VFS bytes / edge policy). See {@link ExportOptions}.
|
||||||
* @returns A versioned, portable `BackupData` document.
|
* @returns A versioned, portable `PortableGraph` document.
|
||||||
* @example
|
* @example
|
||||||
* const backup = await brain.now().export({ collection: id }, { includeVectors: true })
|
* const backup = await brain.now().export({ collection: id }, { includeVectors: true })
|
||||||
*/
|
*/
|
||||||
async export(selector: ExportSelector = {}, options: ExportOptions = {}): Promise<BackupData> {
|
async export(selector: ExportSelector = {}, options: ExportOptions = {}): Promise<PortableGraph> {
|
||||||
this.assertUsable('export')
|
this.assertUsable('export')
|
||||||
return exportGraph(this, this.host.storage, selector, options)
|
return exportGraph(this, this.host.storage, selector, options)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,24 @@
|
||||||
/**
|
/**
|
||||||
* @module db/backup
|
* @module db/portableGraph
|
||||||
* @description Portable graph backup/restore engine for Brainy 8.0 — the
|
* @description Portable graph export/import engine for Brainy 8.0 — the
|
||||||
* `BackupData v1` format plus the `export`/`import` engines that power
|
* `PortableGraph v1` document format plus the `export`/`import` engines that power
|
||||||
* `db.export()` / `brain.export()` (read, at a pinned generation) and
|
* `db.export()` / `brain.export()` (read, at a pinned generation) and
|
||||||
* `brain.import(backup)` (write, applied as one atomic transaction).
|
* `brain.import(graph)` (write, applied as one atomic transaction).
|
||||||
*
|
*
|
||||||
* The wire format is identical to the 7.x line (`formatVersion: 1`), so a
|
* A `PortableGraph` is a self-describing, versioned, **portable** representation
|
||||||
* document written by 7.x imports cleanly here and vice-versa. This is the
|
* of a graph (entities + relations, optionally vectors and file blobs) — the unit
|
||||||
* **portable** round-trip (human-readable, partial-or-whole, cross-version),
|
* Brainy exports for interchange between instances, versions, and products, and the
|
||||||
* distinct from `db.persist()`/`Brainy.load()` (the native whole-brain snapshot,
|
* payload other artifacts embed (e.g. a workbench file's `graph` field). It is NOT
|
||||||
* which preserves generation history) and from `brain.import(file)` (foreign-file
|
* a backup: that role belongs to `db.persist()`/`Brainy.load()`, the native
|
||||||
* ingestion).
|
* whole-brain snapshot which preserves generation history. `PortableGraph` is the
|
||||||
|
* human-readable, partial-or-whole, cross-version round-trip; distinct also from
|
||||||
|
* `brain.import(file)` (foreign-file ingestion).
|
||||||
*
|
*
|
||||||
* **Reserved-field split:** each `BackupEntity` carries Brainy's standard fields
|
* Every document carries `format: 'brainy-portable-graph'` and `formatVersion: 1`;
|
||||||
|
* import gates on `formatVersion` for forward migration. The format is shared
|
||||||
|
* verbatim across the 7.x and 8.0 lines.
|
||||||
|
*
|
||||||
|
* **Reserved-field split:** each `PortableGraphEntity` carries Brainy's standard fields
|
||||||
* (`subtype`, `visibility`, `data`, `confidence`, `weight`, `service`, `createdBy`,
|
* (`subtype`, `visibility`, `data`, `confidence`, `weight`, `service`, `createdBy`,
|
||||||
* `createdAt`) at the TOP LEVEL and `metadata` holds ONLY custom user fields —
|
* `createdAt`) at the TOP LEVEL and `metadata` holds ONLY custom user fields —
|
||||||
* mirroring the in-memory `Entity`, so import maps each to its dedicated `add`/
|
* mirroring the in-memory `Entity`, so import maps each to its dedicated `add`/
|
||||||
|
|
@ -27,10 +33,10 @@ import { TxOperation } from './types.js'
|
||||||
|
|
||||||
/** Fixed entity id of the VFS root collection (a `system` entity; excluded unless `includeSystem`). */
|
/** Fixed entity id of the VFS root collection (a `system` entity; excluded unless `includeSystem`). */
|
||||||
const VFS_ROOT_ID = '00000000-0000-0000-0000-000000000000'
|
const VFS_ROOT_ID = '00000000-0000-0000-0000-000000000000'
|
||||||
/** Magic string identifying a Brainy portable backup document. */
|
/** Magic string identifying a Brainy `PortableGraph` document (the `format` tag). */
|
||||||
export const BACKUP_FORMAT = 'brainy-backup'
|
export const PORTABLE_GRAPH_FORMAT = 'brainy-portable-graph'
|
||||||
/** Current portable-format version (import gates on this for cross-version migration). */
|
/** Current portable-format version (import gates on this for cross-version migration). */
|
||||||
export const BACKUP_FORMAT_VERSION = 1
|
export const PORTABLE_GRAPH_FORMAT_VERSION = 1
|
||||||
/** Default embedding model label (informational; `dimensions` is the real compat gate). */
|
/** Default embedding model label (informational; `dimensions` is the real compat gate). */
|
||||||
const DEFAULT_EMBED_MODEL = 'all-MiniLM-L6-v2'
|
const DEFAULT_EMBED_MODEL = 'all-MiniLM-L6-v2'
|
||||||
/** Page size for find-based enumeration (whole-brain / predicate selectors). */
|
/** Page size for find-based enumeration (whole-brain / predicate selectors). */
|
||||||
|
|
@ -92,7 +98,7 @@ export interface ExportOptions {
|
||||||
edges?: 'induced' | 'incident' | 'none'
|
edges?: 'induced' | 'incident' | 'none'
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Controls how a `BackupData` is applied on `import()`. */
|
/** Controls how a `PortableGraph` is applied on `import()`. */
|
||||||
export interface ImportOptions {
|
export interface ImportOptions {
|
||||||
/** Conflict policy for an existing id (default: `'merge'` — dedup-by-id). */
|
/** Conflict policy for an existing id (default: `'merge'` — dedup-by-id). */
|
||||||
onConflict?: 'merge' | 'replace' | 'skip'
|
onConflict?: 'merge' | 'replace' | 'skip'
|
||||||
|
|
@ -104,8 +110,8 @@ export interface ImportOptions {
|
||||||
meta?: Record<string, unknown>
|
meta?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
/** One entity in a `BackupData`. Standard fields top-level; `metadata` is custom-only. */
|
/** One entity in a `PortableGraph`. Standard fields top-level; `metadata` is custom-only. */
|
||||||
export interface BackupEntity {
|
export interface PortableGraphEntity {
|
||||||
id: string
|
id: string
|
||||||
type: string
|
type: string
|
||||||
subtype?: string
|
subtype?: string
|
||||||
|
|
@ -120,8 +126,8 @@ export interface BackupEntity {
|
||||||
metadata?: any
|
metadata?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
/** One relation (edge) in a `BackupData`. */
|
/** One relation (edge) in a `PortableGraph`. */
|
||||||
export interface BackupRelation {
|
export interface PortableGraphRelation {
|
||||||
id: string
|
id: string
|
||||||
from: string
|
from: string
|
||||||
to: string
|
to: string
|
||||||
|
|
@ -134,15 +140,15 @@ export interface BackupRelation {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A self-describing, versioned, portable graph document (identical on 7.x and 8.0). */
|
/** A self-describing, versioned, portable graph document (identical on 7.x and 8.0). */
|
||||||
export interface BackupData {
|
export interface PortableGraph {
|
||||||
format: typeof BACKUP_FORMAT
|
format: typeof PORTABLE_GRAPH_FORMAT
|
||||||
formatVersion: number
|
formatVersion: number
|
||||||
brainyVersion: string
|
brainyVersion: string
|
||||||
createdAt: string
|
createdAt: string
|
||||||
embedding: { model: string; dimensions: number }
|
embedding: { model: string; dimensions: number }
|
||||||
selector?: ExportSelector
|
selector?: ExportSelector
|
||||||
entities: BackupEntity[]
|
entities: PortableGraphEntity[]
|
||||||
relations: BackupRelation[]
|
relations: PortableGraphRelation[]
|
||||||
blobs?: Record<string, string>
|
blobs?: Record<string, string>
|
||||||
danglingIds?: string[]
|
danglingIds?: string[]
|
||||||
stats: { entityCount: number; relationCount: number; blobCount: number; vectorDimensions?: number }
|
stats: { entityCount: number; relationCount: number; blobCount: number; vectorDimensions?: number }
|
||||||
|
|
@ -159,30 +165,30 @@ export interface ImportResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The minimal read surface the export engine needs — satisfied by `Db` and `Brainy`. */
|
/** The minimal read surface the export engine needs — satisfied by `Db` and `Brainy`. */
|
||||||
export interface BackupReader<T = any> {
|
export interface PortableGraphReader<T = any> {
|
||||||
get(id: string, options?: { includeVectors?: boolean }): Promise<Entity<T> | null>
|
get(id: string, options?: { includeVectors?: boolean }): Promise<Entity<T> | null>
|
||||||
find(query: any): Promise<Result<T>[]>
|
find(query: any): Promise<Result<T>[]>
|
||||||
related(paramsOrId?: any): Promise<Relation<T>[]>
|
related(paramsOrId?: any): Promise<Relation<T>[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The minimal write surface the import engine needs — satisfied by `Brainy`. */
|
/** The minimal write surface the import engine needs — satisfied by `Brainy`. */
|
||||||
export interface BackupWriter<T = any> {
|
export interface PortableGraphWriter<T = any> {
|
||||||
get(id: string, options?: { includeVectors?: boolean }): Promise<Entity<T> | null>
|
get(id: string, options?: { includeVectors?: boolean }): Promise<Entity<T> | null>
|
||||||
transact(ops: TxOperation<T>[], options?: { meta?: Record<string, unknown> }): Promise<unknown>
|
transact(ops: TxOperation<T>[], options?: { meta?: Record<string, unknown> }): Promise<unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Type guard: is this value a Brainy `BackupData` document (vs a file/foreign object)? */
|
/** Type guard: is this value a Brainy `PortableGraph` document (vs a file/foreign object)? */
|
||||||
export function isBackupData(value: unknown): value is BackupData {
|
export function isPortableGraph(value: unknown): value is PortableGraph {
|
||||||
return (
|
return (
|
||||||
typeof value === 'object' &&
|
typeof value === 'object' &&
|
||||||
value !== null &&
|
value !== null &&
|
||||||
(value as any).format === BACKUP_FORMAT &&
|
(value as any).format === PORTABLE_GRAPH_FORMAT &&
|
||||||
Array.isArray((value as any).entities)
|
Array.isArray((value as any).entities)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Result of {@link validateBackup}. `valid` is true iff `errors` is empty. */
|
/** Result of {@link validatePortableGraph}. `valid` is true iff `errors` is empty. */
|
||||||
export interface BackupValidation {
|
export interface PortableGraphValidation {
|
||||||
valid: boolean
|
valid: boolean
|
||||||
errors: string[]
|
errors: string[]
|
||||||
warnings: string[]
|
warnings: string[]
|
||||||
|
|
@ -191,29 +197,29 @@ export interface BackupValidation {
|
||||||
/**
|
/**
|
||||||
* @description Dry-run check a value before `import()`, without mutating the brain:
|
* @description Dry-run check a value before `import()`, without mutating the brain:
|
||||||
* structural validity, a supported `formatVersion`, entity-id uniqueness + required
|
* structural validity, a supported `formatVersion`, entity-id uniqueness + required
|
||||||
* fields, and relation-endpoint coverage. `import()` throws on a non-`BackupData` or a
|
* fields, and relation-endpoint coverage. `import()` throws on a non-`PortableGraph` or a
|
||||||
* too-new `formatVersion`; `validateBackup()` lets a consumer (e.g. a serializer checking
|
* too-new `formatVersion`; `validatePortableGraph()` lets a consumer (e.g. a serializer checking
|
||||||
* a `.wbench` graph payload) surface issues first.
|
* a `.wbench` graph payload) surface issues first.
|
||||||
* @param data - The value to validate (need not be a `BackupData`).
|
* @param data - The value to validate (need not be a `PortableGraph`).
|
||||||
* @returns `{ valid, errors, warnings }`.
|
* @returns `{ valid, errors, warnings }`.
|
||||||
* @example
|
* @example
|
||||||
* const { valid, errors } = validateBackup(payload)
|
* const { valid, errors } = validatePortableGraph(payload)
|
||||||
* if (!valid) throw new Error(`invalid backup: ${errors.join('; ')}`)
|
* if (!valid) throw new Error(`invalid PortableGraph: ${errors.join('; ')}`)
|
||||||
* await brain.import(payload)
|
* await brain.import(payload)
|
||||||
*/
|
*/
|
||||||
export function validateBackup(data: unknown): BackupValidation {
|
export function validatePortableGraph(data: unknown): PortableGraphValidation {
|
||||||
const errors: string[] = []
|
const errors: string[] = []
|
||||||
const warnings: string[] = []
|
const warnings: string[] = []
|
||||||
|
|
||||||
if (!isBackupData(data)) {
|
if (!isPortableGraph(data)) {
|
||||||
errors.push(`not a BackupData document (expected format: '${BACKUP_FORMAT}')`)
|
errors.push(`not a PortableGraph document (expected format: '${PORTABLE_GRAPH_FORMAT}')`)
|
||||||
return { valid: false, errors, warnings }
|
return { valid: false, errors, warnings }
|
||||||
}
|
}
|
||||||
if (typeof data.formatVersion !== 'number') {
|
if (typeof data.formatVersion !== 'number') {
|
||||||
errors.push('formatVersion is missing or not a number')
|
errors.push('formatVersion is missing or not a number')
|
||||||
} else if (data.formatVersion > BACKUP_FORMAT_VERSION) {
|
} else if (data.formatVersion > PORTABLE_GRAPH_FORMAT_VERSION) {
|
||||||
errors.push(
|
errors.push(
|
||||||
`formatVersion ${data.formatVersion} is newer than supported (max ${BACKUP_FORMAT_VERSION})`
|
`formatVersion ${data.formatVersion} is newer than supported (max ${PORTABLE_GRAPH_FORMAT_VERSION})`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -264,7 +270,7 @@ export function validateBackup(data: unknown): BackupValidation {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Serialize part or all of a graph (read through `reader` at its pinned
|
* @description Serialize part or all of a graph (read through `reader` at its pinned
|
||||||
* generation) into a portable `BackupData` document.
|
* generation) into a portable `PortableGraph` document.
|
||||||
* @param reader - Generation-correct read surface (`Db` or `Brainy`).
|
* @param reader - Generation-correct read surface (`Db` or `Brainy`).
|
||||||
* @param storage - Storage adapter (used only for VFS blob bytes when `includeContent`).
|
* @param storage - Storage adapter (used only for VFS blob bytes when `includeContent`).
|
||||||
* @param selector - WHAT to export (omit for the whole brain).
|
* @param selector - WHAT to export (omit for the whole brain).
|
||||||
|
|
@ -272,11 +278,11 @@ export function validateBackup(data: unknown): BackupValidation {
|
||||||
* @param dimensions - Embedding dimensionality for the manifest.
|
* @param dimensions - Embedding dimensionality for the manifest.
|
||||||
*/
|
*/
|
||||||
export async function exportGraph<T = any>(
|
export async function exportGraph<T = any>(
|
||||||
reader: BackupReader<T>,
|
reader: PortableGraphReader<T>,
|
||||||
storage: StorageAdapter | undefined,
|
storage: StorageAdapter | undefined,
|
||||||
selector: ExportSelector,
|
selector: ExportSelector,
|
||||||
options: ExportOptions
|
options: ExportOptions
|
||||||
): Promise<BackupData> {
|
): Promise<PortableGraph> {
|
||||||
const {
|
const {
|
||||||
includeVectors = false,
|
includeVectors = false,
|
||||||
includeContent = false,
|
includeContent = false,
|
||||||
|
|
@ -290,14 +296,14 @@ export async function exportGraph<T = any>(
|
||||||
// 2. Read canonical entities (reserved fields top-level), applying any predicate filter.
|
// 2. Read canonical entities (reserved fields top-level), applying any predicate filter.
|
||||||
const usePredicate = hasPredicate(selector)
|
const usePredicate = hasPredicate(selector)
|
||||||
const entityMap = new Map<string, Entity<T>>()
|
const entityMap = new Map<string, Entity<T>>()
|
||||||
const entities: BackupEntity[] = []
|
const entities: PortableGraphEntity[] = []
|
||||||
for (const id of idSet) {
|
for (const id of idSet) {
|
||||||
const e = await reader.get(id, { includeVectors })
|
const e = await reader.get(id, { includeVectors })
|
||||||
if (!e) continue
|
if (!e) continue
|
||||||
if (!includeSystem && (e as any).visibility === 'system') continue
|
if (!includeSystem && (e as any).visibility === 'system') continue
|
||||||
if (usePredicate && !matchesPredicate(e, selector)) continue
|
if (usePredicate && !matchesPredicate(e, selector)) continue
|
||||||
entityMap.set(id, e)
|
entityMap.set(id, e)
|
||||||
entities.push(toBackupEntity(e, includeVectors))
|
entities.push(toPortableGraphEntity(e, includeVectors))
|
||||||
}
|
}
|
||||||
const keptIds = new Set(entityMap.keys())
|
const keptIds = new Set(entityMap.keys())
|
||||||
|
|
||||||
|
|
@ -314,8 +320,8 @@ export async function exportGraph<T = any>(
|
||||||
const dimensions = entities.find((e) => e.vector && e.vector.length)?.vector?.length ?? 384
|
const dimensions = entities.find((e) => e.vector && e.vector.length)?.vector?.length ?? 384
|
||||||
|
|
||||||
return {
|
return {
|
||||||
format: BACKUP_FORMAT,
|
format: PORTABLE_GRAPH_FORMAT,
|
||||||
formatVersion: BACKUP_FORMAT_VERSION,
|
formatVersion: PORTABLE_GRAPH_FORMAT_VERSION,
|
||||||
brainyVersion: getBrainyVersion(),
|
brainyVersion: getBrainyVersion(),
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
embedding: { model: DEFAULT_EMBED_MODEL, dimensions },
|
embedding: { model: DEFAULT_EMBED_MODEL, dimensions },
|
||||||
|
|
@ -338,31 +344,31 @@ export async function exportGraph<T = any>(
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Apply a `BackupData` to the brain as ONE atomic transaction (a single
|
* @description Apply a `PortableGraph` to the brain as ONE atomic transaction (a single
|
||||||
* new generation). Dedup-by-id merge by default; re-embeds from `data` when no vector
|
* new generation). Dedup-by-id merge by default; re-embeds from `data` when no vector
|
||||||
* is carried. VFS blob bytes are written first (content-addressed, idempotent).
|
* is carried. VFS blob bytes are written first (content-addressed, idempotent).
|
||||||
* @param writer - The brain (`get` + `transact`).
|
* @param writer - The brain (`get` + `transact`).
|
||||||
* @param storage - Storage adapter (for VFS blob bytes).
|
* @param storage - Storage adapter (for VFS blob bytes).
|
||||||
* @param data - A `BackupData` document.
|
* @param data - A `PortableGraph` document.
|
||||||
* @param options - Conflict / vector / id-remap policy.
|
* @param options - Conflict / vector / id-remap policy.
|
||||||
* @throws If `data` is not a `BackupData`, or its `formatVersion` is newer than supported.
|
* @throws If `data` is not a `PortableGraph`, or its `formatVersion` is newer than supported.
|
||||||
*/
|
*/
|
||||||
export async function importGraph<T = any>(
|
export async function importGraph<T = any>(
|
||||||
writer: BackupWriter<T>,
|
writer: PortableGraphWriter<T>,
|
||||||
storage: StorageAdapter | undefined,
|
storage: StorageAdapter | undefined,
|
||||||
data: BackupData,
|
data: PortableGraph,
|
||||||
options: ImportOptions = {}
|
options: ImportOptions = {}
|
||||||
): Promise<ImportResult> {
|
): Promise<ImportResult> {
|
||||||
if (!isBackupData(data)) {
|
if (!isPortableGraph(data)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`import() expects a BackupData document (format:'${BACKUP_FORMAT}'). ` +
|
`import() expects a PortableGraph document (format:'${PORTABLE_GRAPH_FORMAT}'). ` +
|
||||||
`For file ingestion (CSV/PDF/Excel/JSON), pass a file/buffer instead.`
|
`For file ingestion (CSV/PDF/Excel/JSON), pass a file/buffer instead.`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (typeof data.formatVersion === 'number' && data.formatVersion > BACKUP_FORMAT_VERSION) {
|
if (typeof data.formatVersion === 'number' && data.formatVersion > PORTABLE_GRAPH_FORMAT_VERSION) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Backup formatVersion ${data.formatVersion} is newer than this Brainy supports ` +
|
`PortableGraph formatVersion ${data.formatVersion} is newer than this Brainy supports ` +
|
||||||
`(max ${BACKUP_FORMAT_VERSION}). Upgrade Brainy to import this document.`
|
`(max ${PORTABLE_GRAPH_FORMAT_VERSION}). Upgrade Brainy to import this document.`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -445,7 +451,7 @@ export async function importGraph<T = any>(
|
||||||
} as TxOperation<T>)
|
} as TxOperation<T>)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Apply atomically — one generation for the whole backup, or none.
|
// 3. Apply atomically — one generation for the whole graph, or none.
|
||||||
if (ops.length > 0) {
|
if (ops.length > 0) {
|
||||||
await writer.transact(ops, meta ? { meta } : undefined)
|
await writer.transact(ops, meta ? { meta } : undefined)
|
||||||
}
|
}
|
||||||
|
|
@ -472,7 +478,7 @@ function hasPredicate(s: ExportSelector): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveSelector<T>(
|
async function resolveSelector<T>(
|
||||||
reader: BackupReader<T>,
|
reader: PortableGraphReader<T>,
|
||||||
s: ExportSelector,
|
s: ExportSelector,
|
||||||
includeSystem: boolean
|
includeSystem: boolean
|
||||||
): Promise<Set<string>> {
|
): Promise<Set<string>> {
|
||||||
|
|
@ -493,7 +499,7 @@ async function resolveSelector<T>(
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Whole-brain / predicate enumeration via generation-correct paginated `find()`. */
|
/** Whole-brain / predicate enumeration via generation-correct paginated `find()`. */
|
||||||
async function enumerateAll<T>(reader: BackupReader<T>, s: ExportSelector): Promise<Set<string>> {
|
async function enumerateAll<T>(reader: PortableGraphReader<T>, s: ExportSelector): Promise<Set<string>> {
|
||||||
const params: any = {}
|
const params: any = {}
|
||||||
if (s.type !== undefined) params.type = s.type
|
if (s.type !== undefined) params.type = s.type
|
||||||
if (s.subtype !== undefined) params.subtype = s.subtype
|
if (s.subtype !== undefined) params.subtype = s.subtype
|
||||||
|
|
@ -512,7 +518,7 @@ async function enumerateAll<T>(reader: BackupReader<T>, s: ExportSelector): Prom
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveCollectionSubtree<T>(
|
async function resolveCollectionSubtree<T>(
|
||||||
reader: BackupReader<T>,
|
reader: PortableGraphReader<T>,
|
||||||
rootId: string,
|
rootId: string,
|
||||||
depth?: number
|
depth?: number
|
||||||
): Promise<Set<string>> {
|
): Promise<Set<string>> {
|
||||||
|
|
@ -538,7 +544,7 @@ async function resolveCollectionSubtree<T>(
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveConnected<T>(
|
async function resolveConnected<T>(
|
||||||
reader: BackupReader<T>,
|
reader: PortableGraphReader<T>,
|
||||||
c: NonNullable<ExportSelector['connected']>
|
c: NonNullable<ExportSelector['connected']>
|
||||||
): Promise<Set<string>> {
|
): Promise<Set<string>> {
|
||||||
const { from, depth = 1, verbs, direction = 'out' } = c
|
const { from, depth = 1, verbs, direction = 'out' } = c
|
||||||
|
|
@ -562,7 +568,7 @@ async function resolveConnected<T>(
|
||||||
}
|
}
|
||||||
|
|
||||||
async function neighboursOf<T>(
|
async function neighboursOf<T>(
|
||||||
reader: BackupReader<T>,
|
reader: PortableGraphReader<T>,
|
||||||
id: string,
|
id: string,
|
||||||
direction: 'out' | 'in' | 'both',
|
direction: 'out' | 'in' | 'both',
|
||||||
verbs?: VerbType[]
|
verbs?: VerbType[]
|
||||||
|
|
@ -580,7 +586,7 @@ async function neighboursOf<T>(
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveVfsPath<T>(
|
async function resolveVfsPath<T>(
|
||||||
reader: BackupReader<T>,
|
reader: PortableGraphReader<T>,
|
||||||
path: string,
|
path: string,
|
||||||
recursive: boolean,
|
recursive: boolean,
|
||||||
depth?: number
|
depth?: number
|
||||||
|
|
@ -596,7 +602,7 @@ async function resolveVfsPath<T>(
|
||||||
return resolveCollectionSubtree(reader, dirId, depth)
|
return resolveCollectionSubtree(reader, dirId, depth)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveVfsPathToId<T>(reader: BackupReader<T>, path: string): Promise<string | null> {
|
async function resolveVfsPathToId<T>(reader: PortableGraphReader<T>, path: string): Promise<string | null> {
|
||||||
const segments = path.split('/').filter(Boolean)
|
const segments = path.split('/').filter(Boolean)
|
||||||
let currentId = VFS_ROOT_ID
|
let currentId = VFS_ROOT_ID
|
||||||
for (const seg of segments) {
|
for (const seg of segments) {
|
||||||
|
|
@ -645,8 +651,8 @@ function matchesWhere(metadata: any, where: Record<string, any>): boolean {
|
||||||
// Serialization helpers (private)
|
// Serialization helpers (private)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
function toBackupEntity<T>(e: Entity<T>, includeVectors: boolean): BackupEntity {
|
function toPortableGraphEntity<T>(e: Entity<T>, includeVectors: boolean): PortableGraphEntity {
|
||||||
const be: BackupEntity = { id: e.id, type: e.type as string }
|
const be: PortableGraphEntity = { id: e.id, type: e.type as string }
|
||||||
if (e.subtype !== undefined) be.subtype = e.subtype
|
if (e.subtype !== undefined) be.subtype = e.subtype
|
||||||
const vis = (e as any).visibility
|
const vis = (e as any).visibility
|
||||||
if (vis !== undefined && vis !== 'public') be.visibility = vis
|
if (vis !== undefined && vis !== 'public') be.visibility = vis
|
||||||
|
|
@ -661,8 +667,8 @@ function toBackupEntity<T>(e: Entity<T>, includeVectors: boolean): BackupEntity
|
||||||
return be
|
return be
|
||||||
}
|
}
|
||||||
|
|
||||||
function toBackupRelation<T>(r: Relation<T>): BackupRelation {
|
function toPortableGraphRelation<T>(r: Relation<T>): PortableGraphRelation {
|
||||||
const br: BackupRelation = { id: r.id, from: r.from, to: r.to, type: r.type as string }
|
const br: PortableGraphRelation = { id: r.id, from: r.from, to: r.to, type: r.type as string }
|
||||||
if (r.subtype !== undefined) br.subtype = r.subtype
|
if (r.subtype !== undefined) br.subtype = r.subtype
|
||||||
const vis = (r as any).visibility
|
const vis = (r as any).visibility
|
||||||
if (vis !== undefined && vis !== 'public') br.visibility = vis
|
if (vis !== undefined && vis !== 'public') br.visibility = vis
|
||||||
|
|
@ -673,13 +679,13 @@ function toBackupRelation<T>(r: Relation<T>): BackupRelation {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function collectEdges<T>(
|
async function collectEdges<T>(
|
||||||
reader: BackupReader<T>,
|
reader: PortableGraphReader<T>,
|
||||||
idSet: Set<string>,
|
idSet: Set<string>,
|
||||||
edges: 'induced' | 'incident' | 'none'
|
edges: 'induced' | 'incident' | 'none'
|
||||||
): Promise<{ relations: BackupRelation[]; danglingIds?: string[] }> {
|
): Promise<{ relations: PortableGraphRelation[]; danglingIds?: string[] }> {
|
||||||
if (edges === 'none') return { relations: [] }
|
if (edges === 'none') return { relations: [] }
|
||||||
|
|
||||||
const relations: BackupRelation[] = []
|
const relations: PortableGraphRelation[] = []
|
||||||
const dangling = new Set<string>()
|
const dangling = new Set<string>()
|
||||||
const seen = new Set<string>()
|
const seen = new Set<string>()
|
||||||
|
|
||||||
|
|
@ -691,7 +697,7 @@ async function collectEdges<T>(
|
||||||
if (edges === 'induced' && !toIn) continue
|
if (edges === 'induced' && !toIn) continue
|
||||||
if (!toIn) dangling.add(r.to)
|
if (!toIn) dangling.add(r.to)
|
||||||
seen.add(r.id)
|
seen.add(r.id)
|
||||||
relations.push(toBackupRelation(r))
|
relations.push(toPortableGraphRelation(r))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -703,7 +709,7 @@ async function collectEdges<T>(
|
||||||
if (!idSet.has(r.from)) {
|
if (!idSet.has(r.from)) {
|
||||||
dangling.add(r.from)
|
dangling.add(r.from)
|
||||||
seen.add(r.id)
|
seen.add(r.id)
|
||||||
relations.push(toBackupRelation(r))
|
relations.push(toPortableGraphRelation(r))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -740,7 +746,7 @@ async function collectBlobs<T>(
|
||||||
// add()/update() param mapping (private)
|
// add()/update() param mapping (private)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
function entityAddFields(be: BackupEntity): Record<string, any> {
|
function entityAddFields(be: PortableGraphEntity): Record<string, any> {
|
||||||
const fields: Record<string, any> = { data: be.data, type: be.type as NounType }
|
const fields: Record<string, any> = { data: be.data, type: be.type as NounType }
|
||||||
if (be.subtype !== undefined) fields.subtype = be.subtype
|
if (be.subtype !== undefined) fields.subtype = be.subtype
|
||||||
if (be.visibility !== undefined) fields.visibility = be.visibility
|
if (be.visibility !== undefined) fields.visibility = be.visibility
|
||||||
|
|
@ -751,7 +757,7 @@ function entityAddFields(be: BackupEntity): Record<string, any> {
|
||||||
return fields
|
return fields
|
||||||
}
|
}
|
||||||
|
|
||||||
function entityUpdateFields(be: BackupEntity): Record<string, any> {
|
function entityUpdateFields(be: PortableGraphEntity): Record<string, any> {
|
||||||
const fields: Record<string, any> = {}
|
const fields: Record<string, any> = {}
|
||||||
if (be.data !== undefined) fields.data = be.data
|
if (be.data !== undefined) fields.data = be.data
|
||||||
if (be.type !== undefined) fields.type = be.type as NounType
|
if (be.type !== undefined) fields.type = be.type as NounType
|
||||||
16
src/index.ts
16
src/index.ts
|
|
@ -142,19 +142,19 @@ export { EntityNotFoundError, RelationNotFoundError } from './errors/notFound.js
|
||||||
// db.with() / db.persist() / Brainy.load(). See src/db/ for the record layer.
|
// db.with() / db.persist() / Brainy.load(). See src/db/ for the record layer.
|
||||||
export { Db } from './db/db.js'
|
export { Db } from './db/db.js'
|
||||||
|
|
||||||
// Portable graph backup/restore — db.export() / brain.export() / brain.import()
|
// Portable graph export/import — db.export() / brain.export() / brain.import()
|
||||||
// (BackupData v1; identical wire format to the 7.x line).
|
// (PortableGraph v1; identical wire format to the 7.x line).
|
||||||
export { isBackupData, validateBackup } from './db/backup.js'
|
export { isPortableGraph, validatePortableGraph } from './db/portableGraph.js'
|
||||||
export type {
|
export type {
|
||||||
BackupData,
|
PortableGraph,
|
||||||
BackupEntity,
|
PortableGraphEntity,
|
||||||
BackupRelation,
|
PortableGraphRelation,
|
||||||
ExportSelector,
|
ExportSelector,
|
||||||
ExportOptions,
|
ExportOptions,
|
||||||
ImportOptions,
|
ImportOptions,
|
||||||
ImportResult,
|
ImportResult,
|
||||||
BackupValidation
|
PortableGraphValidation
|
||||||
} from './db/backup.js'
|
} from './db/portableGraph.js'
|
||||||
export {
|
export {
|
||||||
GenerationConflictError,
|
GenerationConflictError,
|
||||||
SpeculativeOverlayError,
|
SpeculativeOverlayError,
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,10 @@ import * as path from 'node:path'
|
||||||
import { Brainy } from '../../../src/brainy'
|
import { Brainy } from '../../../src/brainy'
|
||||||
import { createTestConfig } from '../../helpers/test-factory'
|
import { createTestConfig } from '../../helpers/test-factory'
|
||||||
import { NounType, VerbType } from '../../../src/types/graphTypes'
|
import { NounType, VerbType } from '../../../src/types/graphTypes'
|
||||||
import { validateBackup } from '../../../src/db/backup'
|
import { validatePortableGraph } from '../../../src/db/portableGraph'
|
||||||
import type { BackupData } from '../../../src/db/backup'
|
import type { PortableGraph } from '../../../src/db/portableGraph'
|
||||||
|
|
||||||
describe('8.0 portable graph export/import (BackupData v1)', () => {
|
describe('8.0 portable graph export/import (PortableGraph v1)', () => {
|
||||||
let brain: Brainy
|
let brain: Brainy
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
|
@ -33,13 +33,13 @@ describe('8.0 portable graph export/import (BackupData v1)', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('format + round-trip', () => {
|
describe('format + round-trip', () => {
|
||||||
it('brain.export() produces a versioned BackupData document', async () => {
|
it('brain.export() produces a versioned PortableGraph document', async () => {
|
||||||
const a = await brain.add({ data: 'Alice', type: NounType.Person, subtype: 'employee' })
|
const a = await brain.add({ data: 'Alice', type: NounType.Person, subtype: 'employee' })
|
||||||
const b = await brain.add({ data: 'Acme', type: NounType.Organization, subtype: 'vendor' })
|
const b = await brain.add({ data: 'Acme', type: NounType.Organization, subtype: 'vendor' })
|
||||||
await brain.relate({ from: a, to: b, type: VerbType.WorksWith, subtype: 'full-time' })
|
await brain.relate({ from: a, to: b, type: VerbType.WorksWith, subtype: 'full-time' })
|
||||||
|
|
||||||
const backup = await brain.export()
|
const backup = await brain.export()
|
||||||
expect(backup.format).toBe('brainy-backup')
|
expect(backup.format).toBe('brainy-portable-graph')
|
||||||
expect(backup.formatVersion).toBe(1)
|
expect(backup.formatVersion).toBe(1)
|
||||||
expect(backup.entities.map((e) => e.id).sort()).toEqual([a, b].sort())
|
expect(backup.entities.map((e) => e.id).sort()).toEqual([a, b].sort())
|
||||||
expect(backup.relations).toHaveLength(1)
|
expect(backup.relations).toHaveLength(1)
|
||||||
|
|
@ -169,8 +169,8 @@ describe('8.0 portable graph export/import (BackupData v1)', () => {
|
||||||
|
|
||||||
describe('import validation', () => {
|
describe('import validation', () => {
|
||||||
it('rejects a newer formatVersion', async () => {
|
it('rejects a newer formatVersion', async () => {
|
||||||
const future: BackupData = {
|
const future: PortableGraph = {
|
||||||
format: 'brainy-backup',
|
format: 'brainy-portable-graph',
|
||||||
formatVersion: 999,
|
formatVersion: 999,
|
||||||
brainyVersion: 'x',
|
brainyVersion: 'x',
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
|
|
@ -203,9 +203,9 @@ describe('8.0 portable graph export/import (BackupData v1)', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('validateBackup() — dry-run check', () => {
|
describe('validatePortableGraph() — dry-run check', () => {
|
||||||
const valid = (): BackupData => ({
|
const valid = (): PortableGraph => ({
|
||||||
format: 'brainy-backup',
|
format: 'brainy-portable-graph',
|
||||||
formatVersion: 1,
|
formatVersion: 1,
|
||||||
brainyVersion: 'x',
|
brainyVersion: 'x',
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
|
|
@ -216,19 +216,19 @@ describe('8.0 portable graph export/import (BackupData v1)', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('accepts a well-formed backup', () => {
|
it('accepts a well-formed backup', () => {
|
||||||
const r = validateBackup(valid())
|
const r = validatePortableGraph(valid())
|
||||||
expect(r.valid).toBe(true)
|
expect(r.valid).toBe(true)
|
||||||
expect(r.errors).toHaveLength(0)
|
expect(r.errors).toHaveLength(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('rejects a non-BackupData value', () => {
|
it('rejects a non-PortableGraph value', () => {
|
||||||
const r = validateBackup({ entities: [] })
|
const r = validatePortableGraph({ entities: [] })
|
||||||
expect(r.valid).toBe(false)
|
expect(r.valid).toBe(false)
|
||||||
expect(r.errors[0]).toMatch(/BackupData/)
|
expect(r.errors[0]).toMatch(/PortableGraph/)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('rejects a newer formatVersion', () => {
|
it('rejects a newer formatVersion', () => {
|
||||||
const r = validateBackup({ ...valid(), formatVersion: 999 })
|
const r = validatePortableGraph({ ...valid(), formatVersion: 999 })
|
||||||
expect(r.valid).toBe(false)
|
expect(r.valid).toBe(false)
|
||||||
expect(r.errors.some((e) => /formatVersion/.test(e))).toBe(true)
|
expect(r.errors.some((e) => /formatVersion/.test(e))).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
@ -239,7 +239,7 @@ describe('8.0 portable graph export/import (BackupData v1)', () => {
|
||||||
{ id: 'dup', type: NounType.Thing },
|
{ id: 'dup', type: NounType.Thing },
|
||||||
{ id: 'dup', type: NounType.Thing }
|
{ id: 'dup', type: NounType.Thing }
|
||||||
]
|
]
|
||||||
const r = validateBackup(b)
|
const r = validatePortableGraph(b)
|
||||||
expect(r.valid).toBe(false)
|
expect(r.valid).toBe(false)
|
||||||
expect(r.errors.some((e) => /duplicate/.test(e))).toBe(true)
|
expect(r.errors.some((e) => /duplicate/.test(e))).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
@ -247,7 +247,7 @@ describe('8.0 portable graph export/import (BackupData v1)', () => {
|
||||||
it('warns (not errors) on a relation endpoint missing from entities', () => {
|
it('warns (not errors) on a relation endpoint missing from entities', () => {
|
||||||
const b = valid()
|
const b = valid()
|
||||||
b.relations = [{ id: 'r1', from: 'a', to: 'missing', type: VerbType.RelatedTo }]
|
b.relations = [{ id: 'r1', from: 'a', to: 'missing', type: VerbType.RelatedTo }]
|
||||||
const r = validateBackup(b)
|
const r = validatePortableGraph(b)
|
||||||
expect(r.valid).toBe(true) // a dangling endpoint is a warning, not a hard error
|
expect(r.valid).toBe(true) // a dangling endpoint is a warning, not a hard error
|
||||||
expect(r.warnings.some((w) => /missing/.test(w))).toBe(true)
|
expect(r.warnings.some((w) => /missing/.test(w))).toBe(true)
|
||||||
})
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue