@masaori/zod-to-entity-definitions
v1.2.0
Published
A library to define data models using Zod with extended metadata and convert them into framework-agnostic Entity Definitions
Downloads
1,202
Maintainers
Readme
zod-to-entity-definitions
A TypeScript library that allows you to define data models using Zod with extended metadata (Primary Key, Foreign Key, Unique) and convert them into framework-agnostic Entity Definitions (ER models).
Features
- 🔧 Zod Extensions: Add
.pk(),.unique(), and.ref()methods to Zod schemas - 🏗️ Entity & Struct Factories: Define entities and reusable struct types
- 🔄 Automatic Generation: Convert Zod schemas to entity definitions and relations
- 🔒 Type-Safe: Full TypeScript support with strict typing
- 📦 Framework Agnostic: Generate generic entity definitions usable by any framework
- ✅ Validation: Built-in validation for entity nesting and reference integrity
Installation
Installation
Install the package from npm:
npm install @masaori/zod-to-entity-definitions zodQuick Start
import { z } from 'zod';
import {
entity,
struct,
generateEntities,
generateRelations,
} from '@masaori/zod-to-entity-definitions';
// 1. Define a Struct (reusable component)
const Address = struct({
name: 'AddressStruct',
description: 'Common address',
columns: {
city: z.string(),
street: z.string(),
},
});
// 2. Define Entities
const Company = entity({
name: 'Company',
columns: {
id: z.string().pk(),
name: z.string(),
address: Address, // Using struct is OK
},
});
const User = entity({
name: 'User',
columns: {
id: z.string().pk(),
email: z.string().email().unique(),
companyId: z.string().ref(Company), // Reference to Company entity
},
});
// 3. Generate Entity Definitions
const definitions = generateEntities([Company, User]);
// 4. Generate Relations
const relations = generateRelations(definitions);
console.log(JSON.stringify(definitions, null, 2));
console.log(JSON.stringify(relations, null, 2));📝 See the examples directory for complete working examples with generated JSON output.
API Reference
Zod Extensions
The library extends Zod schemas with the following methods:
.pk()
Marks a field as the Primary Key.
id: z.string().pk();.unique()
Marks a field as Unique.
email: z.string().email().unique();.ref(targetEntity, targetColumn?)
Marks a field as a Foreign Key reference to another entity.
targetEntity: Must be a schema created withentity()targetColumn: Optional, defaults to "id"
companyId: z.string().ref(Company);
managerId: z.string().ref(User, 'userId');Factory Functions
entity(config)
Creates an entity schema with metadata.
type EntityConfig<T extends z.ZodRawShape> = {
name: string;
description?: string;
columns: T;
// Composite (multi-column) unique constraints. Column names are type-checked against `columns`.
uniques?: (keyof T & string)[][];
};struct(config)
Creates a struct schema for reusable components.
type StructConfig<T extends z.ZodRawShape> = {
name: string;
description?: string;
columns: T;
};Generator Functions
generateEntities(schemas)
Parses Zod schemas and returns an array of EntityDefinition.
Validation Rules:
- Entities cannot directly embed other entities (must use
.ref()) .ref()must point to valid entity schemas
const definitions = generateEntities([Company, User]);generateRelations(definitions)
Analyzes entity definitions to construct relation maps.
const relations = generateRelations(definitions);Type Definitions
EntityDefinition
type EntityDefinition = {
name: string;
description?: string;
properties: EntityPropertyDefinition[];
};EntityPropertyDefinition
Union type representing different property types:
EntityPropertyDefinitionPrimaryKey: Primary key fieldEntityPropertyDefinitionPrimitive: boolean, number, string, DateEntityPropertyDefinitionTypedStruct: Reference to a struct typeEntityPropertyDefinitionReferencedObject: Foreign key reference
EntityRelation
type EntityRelation = {
entityName: string;
referTos: EntityRelationReferTo[];
referredBys: EntityRelationReferredBy[];
};Advanced Usage
Nullable and Optional Fields
const User = entity({
name: 'User',
columns: {
id: z.string().pk(),
nickname: z.string().optional(), // Optional field
bio: z.string().nullable(), // Nullable field
},
});Array Fields
const User = entity({
name: 'User',
columns: {
id: z.string().pk(),
tags: z.array(z.string()),
scores: z.array(z.number()).optional(),
},
});Enum Fields
const User = entity({
name: 'User',
columns: {
id: z.string().pk(),
role: z.enum(['admin', 'user', 'guest']),
},
});Composite Unique Constraints
Single-column uniqueness uses .unique() on the column. For multi-column (composite) uniqueness, declare it at the entity level with uniques. Column names are type-checked against columns.
const CustomerSegmentProductVariantOffering = entity({
name: 'CustomerSegmentProductVariantOffering',
columns: {
id: z.string().pk(),
productVariantId: z.string().ref(ProductVariant),
customerSegmentId: z.string().ref(CustomerSegment),
price: z.number(),
},
// (productVariantId, customerSegmentId) must be unique together
uniques: [['productVariantId', 'customerSegmentId']],
});The constraint is emitted on the generated EntityDefinition as compositeUniqueConstraints:
{
"name": "CustomerSegmentProductVariantOffering",
"properties": [ /* ... */ ],
"compositeUniqueConstraints": [["productVariantId", "customerSegmentId"]]
}- Multiple constraints are supported:
uniques: [['a', 'b'], ['c', 'd']]. - Each group must list at least 2 distinct, existing columns (otherwise
generateEntitiesthrows). Use.unique()for single-column uniqueness. - When no
uniquesare declared,compositeUniqueConstraintsis omitted from the output (existing consumers are unaffected).
Complex Relations
const Department = entity({
name: 'Department',
columns: {
id: z.string().pk(),
name: z.string(),
},
});
const Employee = entity({
name: 'Employee',
columns: {
id: z.string().pk(),
name: z.string(),
departmentId: z.string().ref(Department),
},
});
const Project = entity({
name: 'Project',
columns: {
id: z.string().pk(),
name: z.string(),
leadId: z.string().ref(Employee),
departmentId: z.string().ref(Department),
},
});Validation Rules
❌ Entity Nesting (Not Allowed)
const User = entity({
name: 'User',
columns: {
id: z.string().pk(),
company: Company, // ❌ Error: Direct entity embedding not allowed
},
});✅ Use References Instead
const User = entity({
name: 'User',
columns: {
id: z.string().pk(),
companyId: z.string().ref(Company), // ✅ Correct: Use .ref()
},
});❌ Invalid References
const Address = struct({
name: 'Address',
columns: { city: z.string() },
});
const User = entity({
name: 'User',
columns: {
id: z.string().pk(),
addressId: z.string().ref(Address), // ❌ Error: Can't reference struct
},
});Development
# Install dependencies
pnpm install
# Run tests
pnpm test
# Run tests in watch mode
pnpm test:watch
# Type checking
pnpm check-types
# Linting
pnpm lint
# Auto-fix lint issues
pnpm lint:fix
# Format code
pnpm format
# Build
pnpm buildCI/CD
This project uses GitHub Actions for continuous integration and deployment.
Workflows
Commit Message Validation (
.github/workflows/commit-check.yml)- Runs on all PRs
- Validates commit messages using Conventional Commits
- Ensures all commits are linear (no merge commits)
Lint and Test (
.github/workflows/lint-test.yml)- Runs on all PRs and pushes to main
- Executes linting, type checking, and tests
- Must pass before merging
Publish to GitHub Packages (
.github/workflows/publish.yml)- Manual workflow trigger only (workflow_dispatch)
- Automatically determines version bump using semantic versioning
- Publishes to GitHub Packages with automatic changelog generation
- Version bumps follow Conventional Commits:
feat:→ minor version bumpfix:,docs:,style:,refactor:,test:,build:,ci:,chore:→ patch version bumpBREAKING CHANGE:→ major version bump
Publishing to GitHub Packages
Publishing is automated via GitHub Actions. To publish a new version:
Required tokens: No additional tokens needed!
- The workflow uses the built-in
GITHUB_TOKENwhich is automatically provided by GitHub Actions - The
GITHUB_TOKENhas the necessary permissions to:- Write to the repository (create tags, update files)
- Publish to GitHub Packages
- Create GitHub releases
- No manual token configuration required - it works out of the box
- The workflow uses the built-in
Trigger the publish workflow:
- Go to the Actions tab in GitHub
- Select "Publish to GitHub Packages" workflow
- Click "Run workflow" on the main branch
Automated process:
- The workflow will analyze commit messages since the last release
- Automatically determine the version bump (major/minor/patch)
- Update
package.jsonandCHANGELOG.md - Create a git tag and GitHub release
- Publish to GitHub Packages
Commit Message Format
All commits must follow the Conventional Commits specification:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]Examples:
feat: add new entity validationfix: correct reference resolutiondocs: update README with examplesfeat!: remove deprecated API(breaking change)
Pre-publish Checklist
The package includes a prepublishOnly script that automatically runs before publishing:
- Type checking (
pnpm check-types) - Linting (
pnpm lint) - Tests (
pnpm test) - Build (
pnpm build)
All these checks must pass before the package can be published.
License
MIT
