@ogentx/store
v0.1.2
Published
Shared persistence layer for OgentDoc - provides PackagePersist and DatabaseServiceResolver for both Engine and Hub
Maintainers
Readme
@ogentx/store
Shared persistence layer for OgentDoc - provides PackagePersist and DatabaseServiceResolver for both Engine and Hub services.
Overview
This package implements the core persistence logic for OgentDoc packages and services. It is designed to be framework-agnostic and works with any Prisma-compatible client through the StorePrismaClient interface.
Key Features
- Package & Service Persistence: Upsert operations with business logic
- Revision History: Complete version control for OgentDoc files
- Service Inheritance: Resolve and merge parent service definitions
- Skill, Dependency, Implementer Management: Full CRUD operations
- Schema Definitions: Package-level schema management
Installation
pnpm add @ogentx/storeQuick Start
import { PackagePersist, StorePrismaClient } from '@ogentx/store';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const store = new PackagePersist(prisma as unknown as StorePrismaClient);
// Create a package revision
const revision = await store.createPackageRevision({
packageId: 123,
rawMarkdown: '# My Package\n...',
version: '1.0.0',
changeLog: 'Initial version',
});
// Get revision history
const revisions = await store.getPackageRevisions({
packageId: 123,
limit: 50,
});Architecture
Layer Architecture
┌─────────────────────────────────────────────────────────────┐
│ Hub / Engine REST API │
│ Controllers handle HTTP requests/responses │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Service Layer (NestJS) │
│ - Transaction support via @nestjs-cls/transactional │
│ - Tenant context via ABAC │
│ - Delegates business logic to PackagePersist │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Store Layer (PackagePersist) │
│ - ALL business logic for package/service persistence │
│ - Revision history management │
│ - Inheritance resolution and merging │
│ - Framework-agnostic (works with any Prisma client) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Database (PostgreSQL) │
│ Package, Service, Skill, Dependency, Implementer, etc. │
│ PackageRevision (revision history) │
└─────────────────────────────────────────────────────────────┘Why This Architecture?
- Single Source of Truth: All business logic lives in
PackagePersist - Reusability: Both Engine and Hub use the same persistence logic
- Testability: Easy to unit test with mock Prisma clients
- Maintainability: Changes to business logic only need to be made once
Core Concepts
1. Document-Level Revision
Key Principle: Revision is at the OgentDoc.md file level, not at the database model level.
OgentDoc.md (Source of Truth)
↓
PackageRevision.rawMarkdown (Snapshot)
↓
Parse → PackageAST → DB Models (Package, Service, Skill, etc.)Why Document-Level?
- ✅ Users work with complete .md files
- ✅ Preserves document structure and comments
- ✅ Simple and reliable
- ✅ Easy to restore and compare
2. Revision Lifecycle
1. User creates/updates OgentDoc.md
↓
2. CLI/API sends rawMarkdown + AST to Hub
↓
3. Hub creates PackageRevision (before updating DB)
↓
4. Hub parses and persists to DB models
↓
5. If release, mark revision as released3. Business Rules
Released Package Immutability
- Immutable fields:
name,version,license,releasedAt,org - Mutable fields:
desc,author(metadata only) - Released packages cannot be deleted and recreated
Released Service Immutability
- Immutable fields:
org,slug,version,inheritedId,visibility,releasedAt - Mutable fields:
name,desc,docs(display information only) - Changes to immutable fields would break dependent services
API Reference
Revision History Methods
createPackageRevision
Create a new package revision. This method should be called BEFORE updating the package in DB.
Features:
- Automatic revision number increment
- Content deduplication (skip if identical to previous)
- SHA-256 hash computation
const revision = await store.createPackageRevision({
packageId: 123,
rawMarkdown: '# My Package\n...',
version: '1.0.0',
changeLog: 'Added new service', // Optional
wasReleased: false, // Optional, default: false
});Returns: The created revision record, or the previous revision if content is identical.
getPackageRevisions
Get package revision history with pagination.
Performance: Does NOT load rawMarkdown by default (for performance).
const revisions = await store.getPackageRevisions({
packageId: 123,
limit: 50, // Optional, default: 50
offset: 0, // Optional, default: 0
releasedOnly: false, // Optional, default: false
});Returns: Array of revision records (without rawMarkdown).
getPackageRevision
Get a specific package revision with full content.
const revision = await store.getPackageRevision(123, 5);
console.log(revision.rawMarkdown); // Complete .md contentReturns: The revision record with rawMarkdown, or null if not found.
comparePackageRevisions
Compare two package revisions and get a unified diff.
const diff = await store.comparePackageRevisions({
packageId: 123,
fromRevision: 3,
toRevision: 5,
});
console.log(diff.patch); // Unified diff format
console.log(diff.additions); // Number of added lines
console.log(diff.deletions); // Number of deleted linesReturns: Diff result with patch and statistics.
restorePackageRevision
Restore package to a specific revision.
IMPORTANT: This creates a NEW revision with the old content. It does NOT modify history.
const pkg = await store.restorePackageRevision({
packageId: 123,
revision: 3,
changeLog: 'Restored from revision 3', // Optional
});Process:
- Load the historical rawMarkdown
- Validate content hash
- Create new revision with old content
- Update package version
Returns: The restored package record.
Package & Service Methods
upsertPackageFromAST
Upsert a package from an OgentDoc AST with business logic.
const pkg = await store.upsertPackageFromAST({
ast: myPackageAST,
tenantId: 1,
org: 'acme',
releasedAt: new Date(), // Optional
});upsertServiceFromAST
Upsert a service from an OgentDoc AST with business logic.
const service = await store.upsertServiceFromAST({
packageId: 1,
service: myServiceAST,
ast: myPackageAST,
tenantId: 1,
org: 'acme',
});resolveAndMergeInheritance
Resolve and merge inheritance for a service.
const merged = await store.resolveAndMergeInheritance({
service: {
slug: 'email-pro',
inherits: { org: 'acme', service: 'email', version: '1.0.0' },
skills: [...]
},
tenantSlug: 'acme'
});Other Methods
upsertPackage()- Upsert a packageupsertService()- Upsert a serviceupsertSkills()- Upsert skills for a serviceupsertDependencies()- Upsert dependencies for a serviceupsertImplementers()- Upsert implementers for a serviceupsertSchemas()- Upsert schema definitions for a packagedeletePackageCascade()- Delete a package and all related entitiesdeleteServiceCascade()- Delete a service and all related entities
Performance Optimization
Content Deduplication
// Content hash is computed automatically
// If content is identical to previous revision, no new revision is created
const revision1 = await store.createPackageRevision({
packageId: 123,
rawMarkdown: '# My Package',
version: '1.0.0',
});
// This will return revision1 (no new revision created)
const revision2 = await store.createPackageRevision({
packageId: 123,
rawMarkdown: '# My Package',
version: '1.0.0',
});
expect(revision1.id).toBe(revision2.id);Lazy Loading
// List queries don't load rawMarkdown (performance)
const revisions = await store.getPackageRevisions({ packageId: 123 });
// Get specific revision loads full content
const revision = await store.getPackageRevision(123, 5);
console.log(revision.rawMarkdown); // AvailableError Handling
All methods throw descriptive errors for common scenarios:
// Revision not found
await store.getPackageRevision(123, 999);
// Error: Revision not found
// Content corruption
await store.restorePackageRevision({ packageId: 123, revision: 1 });
// Error: Revision content corrupted: hash mismatch
// Package not found
await store.restorePackageRevision({ packageId: 999, revision: 1 });
// Error: Package 999 not found
// Circular inheritance
await store.resolveAndMergeInheritance({ ... });
// Error: Circular inheritance detectedTesting
Run tests:
pnpm testRun tests in watch mode:
pnpm test:watchDevelopment
Build:
pnpm buildType check:
pnpm typecheckLint:
pnpm lintType Definitions
PackageRevisionRecord
interface PackageRevisionRecord {
id: number;
packageId: number;
revision: number;
rawMarkdown: string;
version: string;
changeLog?: string;
wasReleased: boolean;
contentHash: string;
createdBy: number;
createdAt: Date;
tenantId_: number;
}RevisionDiff
interface RevisionDiff {
from: number;
to: number;
patch: string;
additions: number;
deletions: number;
}StorePrismaClient
interface StorePrismaClient {
package: IPackageDelegate;
service: IServiceDelegate;
skill: ISkillDelegate;
dependency: IDependencyDelegate;
implementer: IImplementerDelegate;
schemaDef: ISchemaDefDelegate;
packageRevision: IPackageRevisionDelegate;
}License
Apache-2.0
