Migrate all repos into monorepo context folders
Bahn: aisupport, Analyse-O2C-C2S, awesome-bahn-mcp-servers, beam-mcp,
Confluence_Bot, db-planet-mcp-server, O2C-Harness, project-audit,
Projekt-KIQ-HP, teamlandkarte-mcp
Dhive: Jury-Voting
Privat: CV, NoteGraph (NOTE: NoteGraph needs complete redo after consolidation)
Shared: AI-Orchestrator, OrgMyLife, power_skills_and_more
Shared/references: symphony (read-only)
Bahn repos remain available as independent remotes - this monorepo
pulls them in via subtree, the originals are untouched.
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { mkdtemp, rm } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
|
||||
import type { Skill, Person } from '../schemas/types';
|
||||
import {
|
||||
getEntityDir,
|
||||
getEntityPath,
|
||||
readEntity,
|
||||
writeEntity,
|
||||
listEntities,
|
||||
deleteEntity,
|
||||
entityExists,
|
||||
} from './entity-files';
|
||||
|
||||
describe('entity-files', () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'kb-test-'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('getEntityDir', () => {
|
||||
it('maps person to kb/profiles/', () => {
|
||||
expect(getEntityDir('person', '/base')).toBe(join('/base', 'kb', 'profiles'));
|
||||
});
|
||||
|
||||
it('maps experience to kb/experiences/', () => {
|
||||
expect(getEntityDir('experience', '/base')).toBe(join('/base', 'kb', 'experiences'));
|
||||
});
|
||||
|
||||
it('maps skill to kb/skills/', () => {
|
||||
expect(getEntityDir('skill', '/base')).toBe(join('/base', 'kb', 'skills'));
|
||||
});
|
||||
|
||||
it('maps organization to kb/organizations/', () => {
|
||||
expect(getEntityDir('organization', '/base')).toBe(join('/base', 'kb', 'organizations'));
|
||||
});
|
||||
|
||||
it('maps project to kb/projects/', () => {
|
||||
expect(getEntityDir('project', '/base')).toBe(join('/base', 'kb', 'projects'));
|
||||
});
|
||||
|
||||
it('maps certification to kb/certifications/', () => {
|
||||
expect(getEntityDir('certification', '/base')).toBe(join('/base', 'kb', 'certifications'));
|
||||
});
|
||||
|
||||
it('maps tandem to kb/tandems/', () => {
|
||||
expect(getEntityDir('tandem', '/base')).toBe(join('/base', 'kb', 'tandems'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEntityPath', () => {
|
||||
it('constructs correct file path', () => {
|
||||
expect(getEntityPath('skill', 'python', '/base')).toBe(join('/base', 'kb', 'skills', 'python.yaml'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('writeEntity + readEntity round-trip', () => {
|
||||
const skill: Skill = {
|
||||
id: 'typescript',
|
||||
type: 'skill',
|
||||
created: '2025-01-15',
|
||||
modified: '2025-01-15',
|
||||
name: 'TypeScript',
|
||||
category: 'programming-language',
|
||||
level: 'expert',
|
||||
years: 5,
|
||||
};
|
||||
|
||||
it('writes and reads back an entity', async () => {
|
||||
await writeEntity(skill, tempDir);
|
||||
const result = await readEntity('skill', 'typescript', tempDir);
|
||||
expect(result).toEqual(skill);
|
||||
});
|
||||
|
||||
it('rejects invalid entities on write', async () => {
|
||||
const invalid = { id: 'bad', type: 'skill', created: '2025-01-15', modified: '2025-01-15' } as unknown as Skill;
|
||||
await expect(writeEntity(invalid, tempDir)).rejects.toThrow('Validation failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('readEntity errors', () => {
|
||||
it('throws descriptive error when entity not found', async () => {
|
||||
await expect(readEntity('skill', 'nonexistent', tempDir)).rejects.toThrow(
|
||||
/Entity not found: skill "nonexistent"/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('listEntities', () => {
|
||||
it('returns empty array for empty/missing directory', async () => {
|
||||
const ids = await listEntities('skill', tempDir);
|
||||
expect(ids).toEqual([]);
|
||||
});
|
||||
|
||||
it('lists entity IDs from directory', async () => {
|
||||
const skill1: Skill = {
|
||||
id: 'python',
|
||||
type: 'skill',
|
||||
created: '2025-01-15',
|
||||
modified: '2025-01-15',
|
||||
name: 'Python',
|
||||
category: 'programming-language',
|
||||
level: 'expert',
|
||||
};
|
||||
const skill2: Skill = {
|
||||
id: 'rust',
|
||||
type: 'skill',
|
||||
created: '2025-01-15',
|
||||
modified: '2025-01-15',
|
||||
name: 'Rust',
|
||||
category: 'programming-language',
|
||||
level: 'intermediate',
|
||||
};
|
||||
await writeEntity(skill1, tempDir);
|
||||
await writeEntity(skill2, tempDir);
|
||||
|
||||
const ids = await listEntities('skill', tempDir);
|
||||
expect(ids.sort()).toEqual(['python', 'rust']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteEntity', () => {
|
||||
it('deletes an existing entity file', async () => {
|
||||
const skill: Skill = {
|
||||
id: 'go-lang',
|
||||
type: 'skill',
|
||||
created: '2025-01-15',
|
||||
modified: '2025-01-15',
|
||||
name: 'Go',
|
||||
category: 'programming-language',
|
||||
level: 'advanced',
|
||||
};
|
||||
await writeEntity(skill, tempDir);
|
||||
expect(await entityExists('skill', 'go-lang', tempDir)).toBe(true);
|
||||
|
||||
await deleteEntity('skill', 'go-lang', tempDir);
|
||||
expect(await entityExists('skill', 'go-lang', tempDir)).toBe(false);
|
||||
});
|
||||
|
||||
it('throws descriptive error when entity not found', async () => {
|
||||
await expect(deleteEntity('skill', 'nonexistent', tempDir)).rejects.toThrow(
|
||||
/Entity not found: skill "nonexistent"/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('entityExists', () => {
|
||||
it('returns false for non-existent entity', async () => {
|
||||
expect(await entityExists('person', 'nobody', tempDir)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true for existing entity', async () => {
|
||||
const person: Person = {
|
||||
id: 'andre-knie',
|
||||
type: 'person',
|
||||
created: '2025-01-15',
|
||||
modified: '2025-09-01',
|
||||
name: { first: 'Andre', last: 'Knie', display: 'Andre Knie' },
|
||||
};
|
||||
await writeEntity(person, tempDir);
|
||||
expect(await entityExists('person', 'andre-knie', tempDir)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple profiles coexist (Req 1.5)', () => {
|
||||
it('supports multiple person profiles in the same KB', async () => {
|
||||
const person1: Person = {
|
||||
id: 'andre-knie',
|
||||
type: 'person',
|
||||
created: '2025-01-15',
|
||||
modified: '2025-09-01',
|
||||
name: { first: 'Andre', last: 'Knie', display: 'Andre Knie' },
|
||||
};
|
||||
const person2: Person = {
|
||||
id: 'claudia-froldi',
|
||||
type: 'person',
|
||||
created: '2025-01-15',
|
||||
modified: '2025-09-01',
|
||||
name: { first: 'Claudia', last: 'Froldi', display: 'Claudia Froldi' },
|
||||
};
|
||||
|
||||
await writeEntity(person1, tempDir);
|
||||
await writeEntity(person2, tempDir);
|
||||
|
||||
const ids = await listEntities('person', tempDir);
|
||||
expect(ids.sort()).toEqual(['andre-knie', 'claudia-froldi']);
|
||||
|
||||
const read1 = await readEntity('person', 'andre-knie', tempDir);
|
||||
const read2 = await readEntity('person', 'claudia-froldi', tempDir);
|
||||
expect(read1.id).toBe('andre-knie');
|
||||
expect(read2.id).toBe('claudia-froldi');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* Entity file management functions for the knowledge base.
|
||||
*
|
||||
* Maps entity types to directory paths, reads/writes/lists/deletes entity YAML files.
|
||||
* Uses fs/promises for all file operations.
|
||||
*/
|
||||
|
||||
import { readFile, writeFile, readdir, unlink, access, mkdir } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import type { Entity, EntityType } from '../schemas/types';
|
||||
import { serializeEntity, deserializeEntity } from './yaml-utils';
|
||||
import { validateEntity } from '../schemas/validate';
|
||||
|
||||
/**
|
||||
* Maps entity types to their kb/ subdirectory names.
|
||||
*/
|
||||
const ENTITY_TYPE_DIRS: Record<EntityType, string> = {
|
||||
person: 'profiles',
|
||||
experience: 'experiences',
|
||||
skill: 'skills',
|
||||
organization: 'organizations',
|
||||
project: 'projects',
|
||||
certification: 'certifications',
|
||||
tandem: 'tandems',
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the directory path for a given entity type.
|
||||
*/
|
||||
export function getEntityDir(type: EntityType, basePath: string = process.cwd()): string {
|
||||
const dir = ENTITY_TYPE_DIRS[type];
|
||||
return join(basePath, 'kb', dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the full file path for an entity given its type and ID.
|
||||
*/
|
||||
export function getEntityPath(type: EntityType, id: string, basePath: string = process.cwd()): string {
|
||||
return join(getEntityDir(type, basePath), `${id}.yaml`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an entity from its YAML file.
|
||||
* Throws a descriptive error if the file is not found.
|
||||
*/
|
||||
export async function readEntity(type: EntityType, id: string, basePath: string = process.cwd()): Promise<Entity> {
|
||||
const filePath = getEntityPath(type, id, basePath);
|
||||
let content: string;
|
||||
try {
|
||||
content = await readFile(filePath, 'utf-8');
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error && 'code' in err && (err as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
throw new Error(`Entity not found: ${type} "${id}" (expected at ${filePath})`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
return deserializeEntity(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an entity to its YAML file.
|
||||
* Validates the entity before writing; rejects invalid entities with validation errors.
|
||||
*/
|
||||
export async function writeEntity(entity: Entity, basePath: string = process.cwd()): Promise<void> {
|
||||
const result = validateEntity(entity);
|
||||
if (!result.valid) {
|
||||
throw new Error(`Validation failed for entity "${entity.id}": ${result.errors.join('; ')}`);
|
||||
}
|
||||
|
||||
const filePath = getEntityPath(entity.type, entity.id, basePath);
|
||||
const dir = getEntityDir(entity.type, basePath);
|
||||
|
||||
await mkdir(dir, { recursive: true });
|
||||
const yaml = serializeEntity(entity);
|
||||
await writeFile(filePath, yaml, 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all entity IDs in a given type directory.
|
||||
* Returns an array of entity IDs (file names without .yaml extension).
|
||||
*/
|
||||
export async function listEntities(type: EntityType, basePath: string = process.cwd()): Promise<string[]> {
|
||||
const dir = getEntityDir(type, basePath);
|
||||
let files: string[];
|
||||
try {
|
||||
files = await readdir(dir);
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error && 'code' in err && (err as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
return [];
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
return files
|
||||
.filter((f) => f.endsWith('.yaml'))
|
||||
.map((f) => f.replace(/\.yaml$/, ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an entity file.
|
||||
* Throws a descriptive error if the file is not found.
|
||||
*/
|
||||
export async function deleteEntity(type: EntityType, id: string, basePath: string = process.cwd()): Promise<void> {
|
||||
const filePath = getEntityPath(type, id, basePath);
|
||||
try {
|
||||
await unlink(filePath);
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error && 'code' in err && (err as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
throw new Error(`Entity not found: ${type} "${id}" (expected at ${filePath})`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an entity file exists.
|
||||
*/
|
||||
export async function entityExists(type: EntityType, id: string, basePath: string = process.cwd()): Promise<boolean> {
|
||||
const filePath = getEntityPath(type, id, basePath);
|
||||
try {
|
||||
await access(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { serializeEntity, deserializeEntity } from './yaml-utils';
|
||||
import type { Experience, Tandem, Skill, Person } from '../schemas/types';
|
||||
|
||||
describe('yaml-utils', () => {
|
||||
describe('serializeEntity / deserializeEntity round-trip', () => {
|
||||
it('round-trips a skill entity', () => {
|
||||
const skill: Skill = {
|
||||
id: 'python',
|
||||
type: 'skill',
|
||||
created: '2025-01-15',
|
||||
modified: '2025-01-15',
|
||||
name: 'Python',
|
||||
category: 'programming-language',
|
||||
level: 'expert',
|
||||
years: 8,
|
||||
};
|
||||
const yaml = serializeEntity(skill);
|
||||
const result = deserializeEntity(yaml);
|
||||
expect(result).toEqual(skill);
|
||||
});
|
||||
|
||||
it('round-trips an experience with null end date and skillsUsed', () => {
|
||||
const exp: Experience = {
|
||||
id: 'db-cargo-lead',
|
||||
type: 'experience',
|
||||
created: '2025-01-15',
|
||||
modified: '2025-09-01',
|
||||
title: 'Lead IT Innovation',
|
||||
organization: 'db-cargo',
|
||||
start: '2023-04',
|
||||
end: null,
|
||||
achievements: ['Built AI platform', 'Led team of 5'],
|
||||
skillsUsed: ['python', 'machine-learning'],
|
||||
};
|
||||
const yaml = serializeEntity(exp);
|
||||
const result = deserializeEntity(yaml);
|
||||
expect(result).toEqual(exp);
|
||||
});
|
||||
|
||||
it('round-trips a tandem entity with all kebab-case fields', () => {
|
||||
const tandem: Tandem = {
|
||||
id: 'froldi-knie',
|
||||
type: 'tandem',
|
||||
created: '2025-01-15',
|
||||
modified: '2025-04-01',
|
||||
partners: ['andre-knie', 'claudia-froldi'],
|
||||
sharedVision: 'Shared professional vision.',
|
||||
complementarySkills: 'Complementary skills description.',
|
||||
collaborationModel: 'How we collaborate.',
|
||||
combinedNarrative: 'Joint narrative text.',
|
||||
jointCompetencies: ['leadership', 'innovation'],
|
||||
};
|
||||
const yaml = serializeEntity(tandem);
|
||||
const result = deserializeEntity(yaml);
|
||||
expect(result).toEqual(tandem);
|
||||
});
|
||||
|
||||
it('round-trips a person entity with nested objects', () => {
|
||||
const person: Person = {
|
||||
id: 'andre-knie',
|
||||
type: 'person',
|
||||
created: '2025-01-15',
|
||||
modified: '2025-09-01',
|
||||
name: { first: 'Andre', last: 'Knie', display: 'Andre Knie' },
|
||||
contact: { email: 'test@example.com', location: 'Berlin' },
|
||||
summary: 'A multi-line\nsummary text.',
|
||||
languages: [
|
||||
{ language: 'German', level: 'native' },
|
||||
{ language: 'English', level: 'fluent' },
|
||||
],
|
||||
skills: ['python', 'typescript'],
|
||||
experiences: ['db-cargo-lead'],
|
||||
};
|
||||
const yaml = serializeEntity(person);
|
||||
const result = deserializeEntity(yaml);
|
||||
expect(result).toEqual(person);
|
||||
});
|
||||
});
|
||||
|
||||
describe('serializeEntity', () => {
|
||||
it('converts camelCase keys to kebab-case in YAML output', () => {
|
||||
const exp: Experience = {
|
||||
id: 'test-exp',
|
||||
type: 'experience',
|
||||
created: '2025-01-15',
|
||||
modified: '2025-01-15',
|
||||
title: 'Developer',
|
||||
organization: 'test-org',
|
||||
start: '2023-01',
|
||||
end: null,
|
||||
skillsUsed: ['typescript'],
|
||||
};
|
||||
const yaml = serializeEntity(exp);
|
||||
expect(yaml).toContain('skills-used:');
|
||||
expect(yaml).not.toContain('skillsUsed');
|
||||
});
|
||||
|
||||
it('renders null values explicitly', () => {
|
||||
const exp: Experience = {
|
||||
id: 'test-exp',
|
||||
type: 'experience',
|
||||
created: '2025-01-15',
|
||||
modified: '2025-01-15',
|
||||
title: 'Developer',
|
||||
organization: 'test-org',
|
||||
start: '2023-01',
|
||||
end: null,
|
||||
};
|
||||
const yaml = serializeEntity(exp);
|
||||
expect(yaml).toContain('end: null');
|
||||
});
|
||||
|
||||
it('converts tandem-specific fields to kebab-case', () => {
|
||||
const tandem: Tandem = {
|
||||
id: 'froldi-knie',
|
||||
type: 'tandem',
|
||||
created: '2025-01-15',
|
||||
modified: '2025-04-01',
|
||||
partners: ['andre-knie', 'claudia-froldi'],
|
||||
sharedVision: 'Vision text',
|
||||
complementarySkills: 'Skills text',
|
||||
collaborationModel: 'Model text',
|
||||
};
|
||||
const yaml = serializeEntity(tandem);
|
||||
expect(yaml).toContain('shared-vision:');
|
||||
expect(yaml).toContain('complementary-skills:');
|
||||
expect(yaml).toContain('collaboration-model:');
|
||||
expect(yaml).not.toContain('sharedVision');
|
||||
expect(yaml).not.toContain('complementarySkills');
|
||||
expect(yaml).not.toContain('collaborationModel');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deserializeEntity', () => {
|
||||
it('converts kebab-case YAML keys to camelCase', () => {
|
||||
const yaml = `id: test-exp
|
||||
type: experience
|
||||
created: "2025-01-15"
|
||||
modified: "2025-01-15"
|
||||
title: Developer
|
||||
organization: test-org
|
||||
start: "2023-01"
|
||||
end: null
|
||||
skills-used:
|
||||
- typescript
|
||||
- react
|
||||
`;
|
||||
const result = deserializeEntity(yaml);
|
||||
expect(result).toHaveProperty('skillsUsed', ['typescript', 'react']);
|
||||
expect(result).not.toHaveProperty('skills-used');
|
||||
});
|
||||
|
||||
it('parses tandem YAML with kebab-case keys', () => {
|
||||
const yaml = `id: froldi-knie
|
||||
type: tandem
|
||||
created: "2025-01-15"
|
||||
modified: "2025-04-01"
|
||||
partners:
|
||||
- andre-knie
|
||||
- claudia-froldi
|
||||
shared-vision: Vision text
|
||||
complementary-skills: Skills text
|
||||
collaboration-model: Model text
|
||||
combined-narrative: Narrative text
|
||||
joint-competencies:
|
||||
- leadership
|
||||
- innovation
|
||||
`;
|
||||
const result = deserializeEntity(yaml) as Tandem;
|
||||
expect(result.sharedVision).toBe('Vision text');
|
||||
expect(result.complementarySkills).toBe('Skills text');
|
||||
expect(result.collaborationModel).toBe('Model text');
|
||||
expect(result.combinedNarrative).toBe('Narrative text');
|
||||
expect(result.jointCompetencies).toEqual(['leadership', 'innovation']);
|
||||
});
|
||||
|
||||
it('handles null end dates', () => {
|
||||
const yaml = `id: test-exp
|
||||
type: experience
|
||||
created: "2025-01-15"
|
||||
modified: "2025-01-15"
|
||||
title: Developer
|
||||
organization: test-org
|
||||
start: "2023-01"
|
||||
end: null
|
||||
`;
|
||||
const result = deserializeEntity(yaml) as Experience;
|
||||
expect(result.end).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* YAML serialization/deserialization utilities for knowledge base entities.
|
||||
*
|
||||
* Handles conversion between TypeScript camelCase properties and YAML kebab-case keys.
|
||||
* Uses the `yaml` package for parsing and stringifying.
|
||||
*/
|
||||
|
||||
import { stringify, parse } from 'yaml';
|
||||
import type { Entity } from '../schemas/types';
|
||||
|
||||
/**
|
||||
* Map of camelCase TypeScript property names to their kebab-case YAML equivalents.
|
||||
* Only includes fields that differ between the two conventions.
|
||||
*/
|
||||
const CAMEL_TO_KEBAB: Record<string, string> = {
|
||||
skillsUsed: 'skills-used',
|
||||
sharedVision: 'shared-vision',
|
||||
complementarySkills: 'complementary-skills',
|
||||
collaborationModel: 'collaboration-model',
|
||||
combinedNarrative: 'combined-narrative',
|
||||
jointCompetencies: 'joint-competencies',
|
||||
};
|
||||
|
||||
const KEBAB_TO_CAMEL: Record<string, string> = Object.fromEntries(
|
||||
Object.entries(CAMEL_TO_KEBAB).map(([camel, kebab]) => [kebab, camel])
|
||||
);
|
||||
|
||||
/**
|
||||
* Recursively converts object keys from camelCase to kebab-case (where mapped).
|
||||
*/
|
||||
function toKebabKeys(obj: unknown): unknown {
|
||||
if (obj === null || obj === undefined) return obj;
|
||||
if (Array.isArray(obj)) return obj.map(toKebabKeys);
|
||||
if (typeof obj !== 'object') return obj;
|
||||
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
||||
const kebabKey = CAMEL_TO_KEBAB[key] ?? key;
|
||||
result[kebabKey] = toKebabKeys(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively converts object keys from kebab-case to camelCase (where mapped).
|
||||
*/
|
||||
function toCamelKeys(obj: unknown): unknown {
|
||||
if (obj === null || obj === undefined) return obj;
|
||||
if (Array.isArray(obj)) return obj.map(toCamelKeys);
|
||||
if (typeof obj !== 'object') return obj;
|
||||
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
||||
const camelKey = KEBAB_TO_CAMEL[key] ?? key;
|
||||
result[camelKey] = toCamelKeys(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes an entity object to a YAML string.
|
||||
* Converts camelCase keys to kebab-case for the YAML output.
|
||||
* Null values are preserved (rendered as `null` in YAML).
|
||||
*/
|
||||
export function serializeEntity(entity: Entity): string {
|
||||
const kebabObj = toKebabKeys(entity);
|
||||
return stringify(kebabObj, { nullStr: 'null', lineWidth: 0 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes a YAML string to an entity object.
|
||||
* Converts kebab-case keys to camelCase for the TypeScript representation.
|
||||
*/
|
||||
export function deserializeEntity(yamlStr: string): Entity {
|
||||
const parsed = parse(yamlStr);
|
||||
return toCamelKeys(parsed) as Entity;
|
||||
}
|
||||
Reference in New Issue
Block a user