feat(8.0): validateBackup() dry-run + includeContent blob round-trip test + clone test
- validateBackup(data) → { valid, errors, warnings }: structural check, supported
formatVersion, entity-id uniqueness + required fields, relation-endpoint coverage.
Lets consumers (e.g. a serializer checking a .wbench graph) validate before import()
without mutating the brain. Exported from the package root.
- Tests: validateBackup cases, remapIds clone (copy-not-move), and the includeContent
VFS-blob filesystem round-trip (export captures bytes → import writes them).
Completes the #196 export/import surface for the showcase bar (deferred to 8.0.x:
since(prior).export() delta convenience, exportStream/importStream NDJSON).
This commit is contained in:
parent
c2b73d4564
commit
7aad80395e
3 changed files with 195 additions and 2 deletions
|
|
@ -181,6 +181,83 @@ export function isBackupData(value: unknown): value is BackupData {
|
|||
)
|
||||
}
|
||||
|
||||
/** Result of {@link validateBackup}. `valid` is true iff `errors` is empty. */
|
||||
export interface BackupValidation {
|
||||
valid: boolean
|
||||
errors: string[]
|
||||
warnings: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Dry-run check a value before `import()`, without mutating the brain:
|
||||
* structural validity, a supported `formatVersion`, entity-id uniqueness + required
|
||||
* fields, and relation-endpoint coverage. `import()` throws on a non-`BackupData` or a
|
||||
* too-new `formatVersion`; `validateBackup()` lets a consumer (e.g. a serializer checking
|
||||
* a `.wbench` graph payload) surface issues first.
|
||||
* @param data - The value to validate (need not be a `BackupData`).
|
||||
* @returns `{ valid, errors, warnings }`.
|
||||
* @example
|
||||
* const { valid, errors } = validateBackup(payload)
|
||||
* if (!valid) throw new Error(`invalid backup: ${errors.join('; ')}`)
|
||||
* await brain.import(payload)
|
||||
*/
|
||||
export function validateBackup(data: unknown): BackupValidation {
|
||||
const errors: string[] = []
|
||||
const warnings: string[] = []
|
||||
|
||||
if (!isBackupData(data)) {
|
||||
errors.push(`not a BackupData document (expected format: '${BACKUP_FORMAT}')`)
|
||||
return { valid: false, errors, warnings }
|
||||
}
|
||||
if (typeof data.formatVersion !== 'number') {
|
||||
errors.push('formatVersion is missing or not a number')
|
||||
} else if (data.formatVersion > BACKUP_FORMAT_VERSION) {
|
||||
errors.push(
|
||||
`formatVersion ${data.formatVersion} is newer than supported (max ${BACKUP_FORMAT_VERSION})`
|
||||
)
|
||||
}
|
||||
|
||||
const ids = new Set<string>()
|
||||
for (const e of data.entities) {
|
||||
if (!e || typeof e.id !== 'string' || e.id.length === 0) {
|
||||
errors.push('an entity is missing an id')
|
||||
continue
|
||||
}
|
||||
if (ids.has(e.id)) errors.push(`duplicate entity id: ${e.id}`)
|
||||
ids.add(e.id)
|
||||
if (e.type === undefined || e.type === null || (e.type as string) === '') {
|
||||
errors.push(`entity ${e.id} is missing a type`)
|
||||
}
|
||||
}
|
||||
|
||||
const dangling = new Set(data.danglingIds ?? [])
|
||||
for (const r of data.relations) {
|
||||
if (!r || typeof r.id !== 'string') {
|
||||
errors.push('a relation is missing an id')
|
||||
continue
|
||||
}
|
||||
if (r.type === undefined || r.type === null || (r.type as string) === '') {
|
||||
errors.push(`relation ${r.id} is missing a type`)
|
||||
}
|
||||
if (!r.from || !r.to) {
|
||||
errors.push(`relation ${r.id} is missing from/to`)
|
||||
continue
|
||||
}
|
||||
if (!ids.has(r.from) && !dangling.has(r.from)) {
|
||||
warnings.push(`relation ${r.id}: from-endpoint ${r.from} is not in entities`)
|
||||
}
|
||||
if (!ids.has(r.to) && !dangling.has(r.to)) {
|
||||
warnings.push(`relation ${r.id}: to-endpoint ${r.to} is not in entities`)
|
||||
}
|
||||
}
|
||||
|
||||
if (!data.embedding || typeof data.embedding.dimensions !== 'number') {
|
||||
warnings.push('embedding manifest is missing or has no dimensions')
|
||||
}
|
||||
|
||||
return { valid: errors.length === 0, errors, warnings }
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EXPORT
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ export { Db } from './db/db.js'
|
|||
|
||||
// Portable graph backup/restore — db.export() / brain.export() / brain.import()
|
||||
// (BackupData v1; identical wire format to the 7.x line).
|
||||
export { isBackupData } from './db/backup.js'
|
||||
export { isBackupData, validateBackup } from './db/backup.js'
|
||||
export type {
|
||||
BackupData,
|
||||
BackupEntity,
|
||||
|
|
@ -152,7 +152,8 @@ export type {
|
|||
ExportSelector,
|
||||
ExportOptions,
|
||||
ImportOptions,
|
||||
ImportResult
|
||||
ImportResult,
|
||||
BackupValidation
|
||||
} from './db/backup.js'
|
||||
export {
|
||||
GenerationConflictError,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue