refactor: rename BackupData → PortableGraph (the type is interchange, not a backup)
Parity with the 8.0 rename, backported to the 7.x line. The brain.data() export()/import() document type was BackupData, but it is the portable, versioned interchange representation of a graph (entities + relations + optional vectors), NOT a backup — exported for transport between instances, versions, and products. The actual backup is the native snapshot, so "Backup*" mis-signalled. Rename every developer-visible symbol, JSDoc, comment and doc: - BackupData→PortableGraph, BackupEntity→PortableGraphEntity, BackupRelation→PortableGraphRelation, BACKUP_FORMAT[_VERSION]→ PORTABLE_GRAPH_FORMAT[_VERSION], internal toBackup* helpers→toPortableGraph*. - src/api/DataAPI.ts, src/index.ts, src/cli/commands/core.ts, docs, and the test (renamed data-backup.test.ts → data-portable-graph.test.ts). The on-the-wire `format` tag is also renamed 'brainy-backup' → 'brainy-portable-graph': the export/import 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 forward. No deprecated aliases (nothing to alias). 1505 unit green; build clean; data-portable-graph.test.ts 20/20.
This commit is contained in:
parent
5e7379dc41
commit
89036deb20
9 changed files with 98 additions and 95 deletions
|
|
@ -1765,18 +1765,18 @@ await brain.import('https://api.example.com/data.json')
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Export & Import (portable backup)
|
### Export & Import (portable graph)
|
||||||
|
|
||||||
`brain.data()` exposes a portable graph backup/restore API. `export(selector?, options?)`
|
`brain.data()` exposes a portable graph graph/restore API. `export(selector?, options?)`
|
||||||
serializes part or all of the graph to a versioned, portable `BackupData` document;
|
serializes part or all of the graph to a versioned, portable `PortableGraph` document;
|
||||||
`import(backup, options?)` restores it (dedup-by-id merge by default, re-embedding when
|
`import(graph, options?)` restores it (dedup-by-id merge by default, re-embedding when
|
||||||
vectors are absent).
|
vectors are absent).
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const data = await brain.data()
|
const data = await brain.data()
|
||||||
|
|
||||||
// Whole brain → a portable BackupData document
|
// Whole brain → a portable PortableGraph document
|
||||||
const backup = await data.export()
|
const graph = await data.export()
|
||||||
|
|
||||||
// Just one workbench's members, with vectors, then restore elsewhere (merge by id)
|
// Just one workbench's members, with vectors, then restore elsewhere (merge by id)
|
||||||
const subset = await data.export({ ids }, { includeVectors: true })
|
const subset = await data.export({ ids }, { includeVectors: true })
|
||||||
|
|
|
||||||
|
|
@ -457,14 +457,14 @@ await brain.storage.withLock('resource-id', async () => {
|
||||||
|
|
||||||
### Export Data
|
### Export Data
|
||||||
```typescript
|
```typescript
|
||||||
// Export the whole brain to a portable BackupData document
|
// Export the whole brain to a portable PortableGraph document
|
||||||
const backup = await brain.data().then(d => d.export(undefined, { includeVectors: true }))
|
const graph = await brain.data().then(d => d.export(undefined, { includeVectors: true }))
|
||||||
```
|
```
|
||||||
|
|
||||||
### Import Data
|
### Import Data
|
||||||
```typescript
|
```typescript
|
||||||
// Restore a BackupData document (dedup-by-id merge by default)
|
// Restore a PortableGraph document (dedup-by-id merge by default)
|
||||||
await brain.data().then(d => d.import(backup, { onConflict: 'merge' }))
|
await brain.data().then(d => d.import(graph, { onConflict: 'merge' }))
|
||||||
```
|
```
|
||||||
|
|
||||||
See the [Export & Import guide](../guides/export-and-import.md) for partial exports
|
See the [Export & Import guide](../guides/export-and-import.md) for partial exports
|
||||||
|
|
@ -479,7 +479,7 @@ const newBrain = new Brainy({ storage: { type: 's3' } })
|
||||||
await oldBrain.init()
|
await oldBrain.init()
|
||||||
await newBrain.init()
|
await newBrain.init()
|
||||||
|
|
||||||
// Transfer all data via a portable backup
|
// Transfer all data via a portable graph
|
||||||
const data = await oldBrain.data().then(d => d.export(undefined, { includeVectors: true }))
|
const data = await oldBrain.data().then(d => d.export(undefined, { includeVectors: true }))
|
||||||
await newBrain.data().then(d => d.import(data))
|
await newBrain.data().then(d => d.import(data))
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -546,7 +546,7 @@ const appComponent = await brain.findOne({ type: 'ReactComponent', name: 'App' }
|
||||||
const imports = await brain.getRelated(appComponent.id, 'Imports')
|
const imports = await brain.getRelated(appComponent.id, 'Imports')
|
||||||
console.log(`App component imports:`, imports.map(c => c.name))
|
console.log(`App component imports:`, imports.map(c => c.name))
|
||||||
|
|
||||||
// Export the analyzed component graph as a portable BackupData document
|
// Export the analyzed component graph as a portable PortableGraph document
|
||||||
const graph = await brain.data().then(d => d.export({ type: 'ReactComponent' }))
|
const graph = await brain.data().then(d => d.export({ type: 'ReactComponent' }))
|
||||||
console.log(`Exported ${graph.stats.entityCount} components, ${graph.stats.relationCount} edges`)
|
console.log(`Exported ${graph.stats.entityCount} components, ${graph.stats.relationCount} edges`)
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -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. Covers vectors, edge policy, conflict handling, and id remapping.
|
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. Covers vectors, edge policy, conflict handling, and id remapping.
|
||||||
next:
|
next:
|
||||||
- guides/subtypes-and-facets
|
- guides/subtypes-and-facets
|
||||||
- api/README
|
- api/README
|
||||||
|
|
@ -15,19 +15,19 @@ next:
|
||||||
|
|
||||||
`brain.data()` exposes a **portable graph export/import** API. One method serializes a
|
`brain.data()` exposes a **portable graph export/import** API. One method serializes a
|
||||||
graph — an item, a collection, a connected neighbourhood, a VFS subtree, a predicate
|
graph — an item, a collection, a connected neighbourhood, a VFS subtree, a predicate
|
||||||
match, or the whole brain — into a single versioned JSON document (`BackupData`); the
|
match, or the whole brain — into a single versioned JSON document (`PortableGraph`); the
|
||||||
inverse restores it.
|
inverse restores it.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const data = await brain.data()
|
const data = await brain.data()
|
||||||
|
|
||||||
const backup = await data.export() // whole brain → BackupData
|
const graph = await data.export() // whole brain → PortableGraph
|
||||||
await data.import(backup) // restore (merge by id, re-embed if no vectors)
|
await data.import(graph) // restore (merge by id, re-embed if no vectors)
|
||||||
```
|
```
|
||||||
|
|
||||||
It is **portable** (human-readable JSON), **versioned** (`formatVersion`, so a 7.x export
|
It is **portable** (human-readable JSON), **versioned** (`formatVersion`, so a 7.x export
|
||||||
imports cleanly into 8.0), and **current-state** (the entities and edges as they are now —
|
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, cross-environment
|
no generation history). Use it for portable artifacts, partial graphs, cross-environment
|
||||||
moves, and version upgrades.
|
moves, and version upgrades.
|
||||||
|
|
||||||
## When to use which
|
## When to use which
|
||||||
|
|
@ -38,13 +38,13 @@ moves, and version upgrades.
|
||||||
| 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) |
|
||||||
|
|
||||||
The two are different operations that happen to share a verb: `import(file)` parses a
|
The two are different operations that happen to share a verb: `import(file)` parses a
|
||||||
foreign document into new entities; `data().import(backup)` restores a graph this API
|
foreign document into new entities; `data().import(graph)` restores a graph this API
|
||||||
exported.
|
exported.
|
||||||
|
|
||||||
## Exporting
|
## Exporting
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export(selector?, options?): Promise<BackupData>
|
export(selector?, options?): Promise<PortableGraph>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Selectors — *what* to export
|
### Selectors — *what* to export
|
||||||
|
|
@ -96,17 +96,17 @@ const tree = await data.export({ vfsPath: '/docs' }, { includeContent: true })
|
||||||
## Importing
|
## Importing
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import(backup, options?): Promise<ImportResult>
|
import(graph, options?): Promise<ImportResult>
|
||||||
```
|
```
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const result = await brain.data().then(d => d.import(backup, { onConflict: 'merge' }))
|
const result = await brain.data().then(d => d.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 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. |
|
||||||
|
|
||||||
|
|
@ -115,15 +115,15 @@ exported documents that share entity ids — re-importing an id merges rather th
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Clone a subgraph under fresh ids (a copy, not a move)
|
// Clone a subgraph under fresh ids (a copy, not a move)
|
||||||
const remap = new Map(backup.entities.map(e => [e.id, crypto.randomUUID()]))
|
const remap = new Map(graph.entities.map(e => [e.id, crypto.randomUUID()]))
|
||||||
await data.import(backup, { remapIds: id => remap.get(id) ?? id })
|
await data.import(graph, { remapIds: id => remap.get(id) ?? id })
|
||||||
```
|
```
|
||||||
|
|
||||||
## The `BackupData` format
|
## The `PortableGraph` format
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
{
|
{
|
||||||
"format": "brainy-backup",
|
"format": "brainy-portable-graph",
|
||||||
"formatVersion": 1, // import gates on this (cross-version migration)
|
"formatVersion": 1, // import gates on this (cross-version migration)
|
||||||
"brainyVersion": "7.32.0",
|
"brainyVersion": "7.32.0",
|
||||||
"createdAt": "2026-06-16T…Z",
|
"createdAt": "2026-06-16T…Z",
|
||||||
|
|
@ -154,7 +154,7 @@ of each entity; `metadata` holds **only** custom user fields — mirroring the i
|
||||||
|
|
||||||
## 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 graph 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). The format is **current-state** — if you need a
|
(or `true` carries vectors verbatim). The format is **current-state** — if you need a
|
||||||
|
|
|
||||||
|
|
@ -540,11 +540,11 @@ export async function generateStaticProps() {
|
||||||
})
|
})
|
||||||
await brain.init()
|
await brain.init()
|
||||||
|
|
||||||
// Build a search index — export the whole brain as a portable BackupData document
|
// Build a search index — export the whole brain as a portable PortableGraph document
|
||||||
const backup = await brain.data().then(d => d.export())
|
const graph = await brain.data().then(d => d.export())
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: { searchIndex: backup.entities }
|
props: { searchIndex: graph.entities }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
/**
|
/**
|
||||||
* @module DataAPI
|
* @module DataAPI
|
||||||
* @description Portable graph backup/restore for Brainy — the `BackupData v1`
|
* @description Portable graph export/import for Brainy — the `PortableGraph v1`
|
||||||
* export/import format plus `clear()`/`getStats()` data-management helpers.
|
* format plus `clear()`/`getStats()` data-management helpers.
|
||||||
*
|
*
|
||||||
* Accessed via `brain.data()`. The two headline methods are:
|
* A `PortableGraph` is the portable, versioned interchange representation of a graph
|
||||||
|
* (entities + relations + optional vectors), NOT a backup — exported for transport
|
||||||
|
* between instances, versions, and products. Accessed via `brain.data()`. The two
|
||||||
|
* headline methods are:
|
||||||
*
|
*
|
||||||
* - **`export(selector?, options?) → BackupData`** — serialize a graph (an item, a
|
* - **`export(selector?, options?) → PortableGraph`** — serialize a graph (an item, a
|
||||||
* collection + children, a connected neighbourhood, a VFS subtree, a predicate
|
* collection + children, a connected neighbourhood, a VFS subtree, a predicate
|
||||||
* match, or the whole brain) into ONE versioned, portable JSON document.
|
* match, or the whole brain) into ONE versioned, portable JSON document.
|
||||||
* - **`import(backup, options?) → ImportResult`** — restore a `BackupData` into the
|
* - **`import(graph, options?) → ImportResult`** — restore a `PortableGraph` into the
|
||||||
* brain (dedup-by-id merge by default), re-embedding from `data` when vectors are
|
* brain (dedup-by-id merge by default), re-embedding from `data` when vectors are
|
||||||
* absent.
|
* absent.
|
||||||
*
|
*
|
||||||
|
|
@ -20,7 +23,7 @@
|
||||||
*
|
*
|
||||||
* **Design decision (reserved-field split):** entities carry Brainy's standard fields
|
* **Design decision (reserved-field split):** entities carry Brainy's standard fields
|
||||||
* (`subtype`, `data`, `confidence`, `weight`, `service`, `createdBy`, `createdAt`) at
|
* (`subtype`, `data`, `confidence`, `weight`, `service`, `createdBy`, `createdAt`) at
|
||||||
* the TOP LEVEL of each `BackupEntity`, and `metadata` holds ONLY custom user fields —
|
* the TOP LEVEL of each `PortableGraphEntity`, and `metadata` holds ONLY custom user fields —
|
||||||
* mirroring the in-memory `Entity` shape, so `import()` maps each field to its dedicated
|
* mirroring the in-memory `Entity` shape, so `import()` maps each field to its dedicated
|
||||||
* `add()`/`relate()` parameter rather than dumping everything into the metadata bag.
|
* `add()`/`relate()` parameter rather than dumping everything into the metadata bag.
|
||||||
*/
|
*/
|
||||||
|
|
@ -32,10 +35,10 @@ import { getBrainyVersion } from '../utils/version.js'
|
||||||
|
|
||||||
/** The fixed entity id of the VFS root collection (excluded from exports unless `includeSystem`). */
|
/** The fixed entity id of the VFS root collection (excluded from exports 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). */
|
||||||
const BACKUP_FORMAT = 'brainy-backup'
|
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. */
|
||||||
const BACKUP_FORMAT_VERSION = 1
|
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'
|
||||||
/** Storage-layer fetch ceiling for enumerations (well above any single-brain entity count). */
|
/** Storage-layer fetch ceiling for enumerations (well above any single-brain entity count). */
|
||||||
|
|
@ -109,12 +112,12 @@ export interface ExportOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Controls how a `BackupData` is applied to the brain on `import()`.
|
* @description Controls how a `PortableGraph` is applied to the brain on `import()`.
|
||||||
*/
|
*/
|
||||||
export interface ImportOptions {
|
export interface ImportOptions {
|
||||||
/**
|
/**
|
||||||
* Conflict policy when an entity id already exists (default: `'merge'`):
|
* Conflict policy when an entity id already exists (default: `'merge'`):
|
||||||
* - `'merge'` — update in place (dedup-by-id; the default that lets you assemble many backups).
|
* - `'merge'` — update in place (dedup-by-id; the default that lets you assemble many graphs).
|
||||||
* - `'replace'` — delete then re-create.
|
* - `'replace'` — delete then re-create.
|
||||||
* - `'skip'` — leave the existing entity untouched.
|
* - `'skip'` — leave the existing entity untouched.
|
||||||
*/
|
*/
|
||||||
|
|
@ -129,8 +132,8 @@ export interface ImportOptions {
|
||||||
remapIds?: (id: string) => string
|
remapIds?: (id: string) => string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 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
|
||||||
/** NounType value. */
|
/** NounType value. */
|
||||||
type: string
|
type: string
|
||||||
|
|
@ -156,8 +159,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
|
||||||
|
|
@ -179,9 +182,9 @@ export interface BackupRelation {
|
||||||
* @description A self-describing, versioned, portable graph document. The same shape
|
* @description A self-describing, versioned, portable graph document. The same shape
|
||||||
* is produced/consumed on 7.x and 8.0; `formatVersion` gates cross-version migration.
|
* is produced/consumed on 7.x and 8.0; `formatVersion` gates cross-version migration.
|
||||||
*/
|
*/
|
||||||
export interface BackupData {
|
export interface PortableGraph {
|
||||||
/** Always `'brainy-backup'` — identifies the document type. */
|
/** Always `'brainy-portable-graph'` — identifies the document type. */
|
||||||
format: typeof BACKUP_FORMAT
|
format: typeof PORTABLE_GRAPH_FORMAT
|
||||||
/** Integer format version (import gates on this). */
|
/** Integer format version (import gates on this). */
|
||||||
formatVersion: number
|
formatVersion: number
|
||||||
/** The Brainy version that produced the document (informational). */
|
/** The Brainy version that produced the document (informational). */
|
||||||
|
|
@ -193,9 +196,9 @@ export interface BackupData {
|
||||||
/** Echo of the selector that produced this document (provenance). */
|
/** Echo of the selector that produced this document (provenance). */
|
||||||
selector?: ExportSelector
|
selector?: ExportSelector
|
||||||
/** Exported entities. */
|
/** Exported entities. */
|
||||||
entities: BackupEntity[]
|
entities: PortableGraphEntity[]
|
||||||
/** Exported relations. */
|
/** Exported relations. */
|
||||||
relations: BackupRelation[]
|
relations: PortableGraphRelation[]
|
||||||
/** VFS file bytes keyed by sha256 — present only with `includeContent`. */
|
/** VFS file bytes keyed by sha256 — present only with `includeContent`. */
|
||||||
blobs?: Record<string, string>
|
blobs?: Record<string, string>
|
||||||
/** Endpoints referenced by `edges:'incident'` that fell outside the node set. */
|
/** Endpoints referenced by `edges:'incident'` that fell outside the node set. */
|
||||||
|
|
@ -244,10 +247,10 @@ export class DataAPI {
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Serialize part or all of the graph into a portable `BackupData`.
|
* @description Serialize part or all of the graph into a portable `PortableGraph`.
|
||||||
* @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, file bytes, edge policy). See {@link ExportOptions}.
|
* @param options - HOW to export (vectors, file bytes, edge policy). See {@link ExportOptions}.
|
||||||
* @returns A versioned, portable `BackupData` document.
|
* @returns A versioned, portable `PortableGraph` document.
|
||||||
* @example
|
* @example
|
||||||
* // A single workbench's members (exact id set), with vectors:
|
* // A single workbench's members (exact id set), with vectors:
|
||||||
* const backup = await brain.data().export({ ids }, { includeVectors: true })
|
* const backup = await brain.data().export({ ids }, { includeVectors: true })
|
||||||
|
|
@ -261,7 +264,7 @@ export class DataAPI {
|
||||||
* // The whole brain:
|
* // The whole brain:
|
||||||
* const backup = await brain.data().export()
|
* const backup = await brain.data().export()
|
||||||
*/
|
*/
|
||||||
async export(selector: ExportSelector = {}, options: ExportOptions = {}): Promise<BackupData> {
|
async export(selector: ExportSelector = {}, options: ExportOptions = {}): Promise<PortableGraph> {
|
||||||
if (!this.brain) {
|
if (!this.brain) {
|
||||||
throw new Error('DataAPI.export() requires a Brainy instance (use brain.data().export()).')
|
throw new Error('DataAPI.export() requires a Brainy instance (use brain.data().export()).')
|
||||||
}
|
}
|
||||||
|
|
@ -291,10 +294,10 @@ export class DataAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Build entity records.
|
// 4. Build entity records.
|
||||||
const entities: BackupEntity[] = []
|
const entities: PortableGraphEntity[] = []
|
||||||
for (const id of idSet) {
|
for (const id of idSet) {
|
||||||
const e = entityMap.get(id)
|
const e = entityMap.get(id)
|
||||||
if (e) entities.push(this.toBackupEntity(e, includeVectors))
|
if (e) entities.push(this.toPortableGraphEntity(e, includeVectors))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Collect edges per policy.
|
// 5. Collect edges per policy.
|
||||||
|
|
@ -315,8 +318,8 @@ export class DataAPI {
|
||||||
const blobCount = blobs ? Object.keys(blobs).length : 0
|
const blobCount = blobs ? Object.keys(blobs).length : 0
|
||||||
|
|
||||||
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: this.detectModel(), dimensions },
|
embedding: { model: this.detectModel(), dimensions },
|
||||||
|
|
@ -339,30 +342,30 @@ export class DataAPI {
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Restore a `BackupData` into the brain. Dedup-by-id merge by default, so
|
* @description Restore a `PortableGraph` into the brain. Dedup-by-id merge by default, so
|
||||||
* assembling many backups that share entity ids merges rather than duplicates. Vectors
|
* assembling many backups that share entity ids merges rather than duplicates. Vectors
|
||||||
* are re-embedded from `data` when absent (`reembed:'auto'`).
|
* are re-embedded from `data` when absent (`reembed:'auto'`).
|
||||||
* @param data - A `BackupData` document (must have `format:'brainy-backup'`).
|
* @param data - A `PortableGraph` document (must have `format:'brainy-portable-graph'`).
|
||||||
* @param options - Conflict/vector/id-remap policy. See {@link ImportOptions}.
|
* @param options - Conflict/vector/id-remap policy. See {@link ImportOptions}.
|
||||||
* @returns Counts of imported/merged/skipped/re-embedded entities + any per-record errors.
|
* @returns Counts of imported/merged/skipped/re-embedded entities + any per-record errors.
|
||||||
* @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.
|
||||||
* @example
|
* @example
|
||||||
* const result = await brain.data().import(backup, { onConflict: 'merge' })
|
* const result = await brain.data().import(backup, { onConflict: 'merge' })
|
||||||
*/
|
*/
|
||||||
async import(data: BackupData, options: ImportOptions = {}): Promise<ImportResult> {
|
async import(data: PortableGraph, options: ImportOptions = {}): Promise<ImportResult> {
|
||||||
if (!this.brain) {
|
if (!this.brain) {
|
||||||
throw new Error('DataAPI.import() requires a Brainy instance (use brain.data().import()).')
|
throw new Error('DataAPI.import() requires a Brainy instance (use brain.data().import()).')
|
||||||
}
|
}
|
||||||
if (!data || (data as any).format !== BACKUP_FORMAT) {
|
if (!data || (data as any).format !== PORTABLE_GRAPH_FORMAT) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`DataAPI.import() expects a BackupData document (format:'${BACKUP_FORMAT}'). ` +
|
`DataAPI.import() expects a PortableGraph document (format:'${PORTABLE_GRAPH_FORMAT}'). ` +
|
||||||
`For file ingestion (CSV/PDF/Excel/JSON), use brain.import() instead.`
|
`For file ingestion (CSV/PDF/Excel/JSON), use brain.import() 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.`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -698,9 +701,9 @@ export class DataAPI {
|
||||||
// SERIALIZATION HELPERS (private)
|
// SERIALIZATION HELPERS (private)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/** Map a canonical `Entity` to a `BackupEntity` (reserved fields top-level). */
|
/** Map a canonical `Entity` to a `PortableGraphEntity` (reserved fields top-level). */
|
||||||
private toBackupEntity(e: Entity, includeVectors: boolean): BackupEntity {
|
private toPortableGraphEntity(e: Entity, 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
|
||||||
|
|
@ -715,9 +718,9 @@ export class DataAPI {
|
||||||
return be
|
return be
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Map a canonical `Relation` to a `BackupRelation`. */
|
/** Map a canonical `Relation` to a `PortableGraphRelation`. */
|
||||||
private toBackupRelation(r: Relation): BackupRelation {
|
private toPortableGraphRelation(r: Relation): 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
|
||||||
|
|
@ -731,10 +734,10 @@ export class DataAPI {
|
||||||
private async collectEdges(
|
private async collectEdges(
|
||||||
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>()
|
||||||
|
|
||||||
|
|
@ -747,7 +750,7 @@ export class DataAPI {
|
||||||
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(this.toBackupRelation(r))
|
relations.push(this.toPortableGraphRelation(r))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -760,7 +763,7 @@ export class DataAPI {
|
||||||
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(this.toBackupRelation(r))
|
relations.push(this.toPortableGraphRelation(r))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -771,7 +774,7 @@ export class DataAPI {
|
||||||
|
|
||||||
/** Read VFS file bytes (base64) for file entities, keyed by content hash. */
|
/** Read VFS file bytes (base64) for file entities, keyed by content hash. */
|
||||||
private async collectBlobs(
|
private async collectBlobs(
|
||||||
entities: BackupEntity[],
|
entities: PortableGraphEntity[],
|
||||||
entityMap: Map<string, Entity>
|
entityMap: Map<string, Entity>
|
||||||
): Promise<Record<string, string>> {
|
): Promise<Record<string, string>> {
|
||||||
const blobs: Record<string, string> = {}
|
const blobs: Record<string, string> = {}
|
||||||
|
|
@ -820,8 +823,8 @@ export class DataAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** `add()` params from a `BackupEntity` (excludes brain-managed fields like `createdAt`). */
|
/** `add()` params from a `PortableGraphEntity` (excludes brain-managed fields like `createdAt`). */
|
||||||
private entityAddFields(be: BackupEntity): Record<string, any> {
|
private entityAddFields(be: PortableGraphEntity): Record<string, any> {
|
||||||
const fields: Record<string, any> = {
|
const fields: Record<string, any> = {
|
||||||
data: be.data,
|
data: be.data,
|
||||||
type: be.type as NounType
|
type: be.type as NounType
|
||||||
|
|
@ -834,8 +837,8 @@ export class DataAPI {
|
||||||
return fields
|
return fields
|
||||||
}
|
}
|
||||||
|
|
||||||
/** `update()` params from a `BackupEntity` (for `onConflict:'merge'`). */
|
/** `update()` params from a `PortableGraphEntity` (for `onConflict:'merge'`). */
|
||||||
private entityUpdateFields(be: BackupEntity): Record<string, any> {
|
private 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
|
||||||
|
|
|
||||||
|
|
@ -876,7 +876,7 @@ export const coreCommands = {
|
||||||
const brain = getBrainy()
|
const brain = getBrainy()
|
||||||
const format = options.format || 'json'
|
const format = options.format || 'json'
|
||||||
|
|
||||||
// Export the whole brain as a portable BackupData document (vectors + VFS file bytes).
|
// Export the whole brain as a portable PortableGraph document (vectors + VFS file bytes).
|
||||||
const dataApi = await brain.data()
|
const dataApi = await brain.data()
|
||||||
const backup = await dataApi.export(undefined, {
|
const backup = await dataApi.export(undefined, {
|
||||||
includeVectors: true,
|
includeVectors: true,
|
||||||
|
|
|
||||||
|
|
@ -116,12 +116,12 @@ export {
|
||||||
// Export version utilities
|
// Export version utilities
|
||||||
export { getBrainyVersion } from './utils/version.js'
|
export { getBrainyVersion } from './utils/version.js'
|
||||||
|
|
||||||
// Export portable graph backup/restore API (brain.data())
|
// Export portable graph export/import API (brain.data())
|
||||||
export { DataAPI } from './api/DataAPI.js'
|
export { DataAPI } from './api/DataAPI.js'
|
||||||
export type {
|
export type {
|
||||||
BackupData,
|
PortableGraph,
|
||||||
BackupEntity,
|
PortableGraphEntity,
|
||||||
BackupRelation,
|
PortableGraphRelation,
|
||||||
ExportSelector,
|
ExportSelector,
|
||||||
ExportOptions,
|
ExportOptions,
|
||||||
ImportOptions,
|
ImportOptions,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* Unit tests for the portable graph backup/restore API (brain.data()).
|
* Unit tests for the portable graph backup/restore API (brain.data()).
|
||||||
*
|
*
|
||||||
* Exercises BackupData v1 export()/import(): real round-trips against in-memory
|
* Exercises PortableGraph v1 export()/import(): real round-trips against in-memory
|
||||||
* storage (no mocks of the API under test) — selectors, edge policy, vectors,
|
* storage (no mocks of the API under test) — selectors, edge policy, vectors,
|
||||||
* conflict handling, id remapping, subtype fidelity, and cross-version guards.
|
* conflict handling, id remapping, subtype fidelity, and cross-version guards.
|
||||||
*/
|
*/
|
||||||
|
|
@ -11,11 +11,11 @@ import { randomUUID } from 'node:crypto'
|
||||||
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 type { BackupData } from '../../../src/api/DataAPI'
|
import type { PortableGraph } from '../../../src/api/DataAPI'
|
||||||
|
|
||||||
const VFS_ROOT_ID = '00000000-0000-0000-0000-000000000000'
|
const VFS_ROOT_ID = '00000000-0000-0000-0000-000000000000'
|
||||||
|
|
||||||
describe('DataAPI — portable graph backup/restore (BackupData v1)', () => {
|
describe('DataAPI — portable graph backup/restore (PortableGraph v1)', () => {
|
||||||
let brain: Brainy
|
let brain: Brainy
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
|
@ -28,14 +28,14 @@ describe('DataAPI — portable graph backup/restore (BackupData v1)', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('format + whole-brain round-trip', () => {
|
describe('format + whole-brain round-trip', () => {
|
||||||
it('exports a self-describing, versioned BackupData document', async () => {
|
it('exports a self-describing, 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 })
|
const b = await brain.add({ data: 'Acme', type: NounType.Organization })
|
||||||
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.data().then((d) => d.export())
|
const backup = await brain.data().then((d) => d.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(typeof backup.brainyVersion).toBe('string')
|
expect(typeof backup.brainyVersion).toBe('string')
|
||||||
expect(backup.brainyVersion.length).toBeGreaterThan(0)
|
expect(backup.brainyVersion.length).toBeGreaterThan(0)
|
||||||
|
|
@ -288,15 +288,15 @@ describe('DataAPI — portable graph backup/restore (BackupData v1)', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('import validation', () => {
|
describe('import validation', () => {
|
||||||
it('rejects a non-BackupData payload', async () => {
|
it('rejects a non-PortableGraph payload', async () => {
|
||||||
const d = await brain.data()
|
const d = await brain.data()
|
||||||
await expect(d.import({ entities: [] } as any)).rejects.toThrow(/BackupData/)
|
await expect(d.import({ entities: [] } as any)).rejects.toThrow(/PortableGraph/)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('rejects a newer formatVersion', async () => {
|
it('rejects a newer formatVersion', async () => {
|
||||||
const d = await brain.data()
|
const d = await brain.data()
|
||||||
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(),
|
||||||
Loading…
Add table
Add a link
Reference in a new issue