| name | update-data-model |
| description | Add or modify domain entities in the data model using Zod schemas. Use when creating new entities, adding fields, changing relationships, or updating the domain model structure. Covers schema creation, TypeScript types, and updating dependent files. |
Skill: Updating the Data Model
This skill covers how to work with the domain model in the model/ directory, which uses Zod for schema validation and TypeScript for type definitions.
Model Architecture
The model uses a one entity per file structure with Zod schemas:
model/
├── index.ts # Barrel export (update when adding entities)
├── enums.ts # Shared enums
├── User.ts # User entity + schema
├── Job.ts # Job entity + schema
└── Site.ts # Site entity + schema
Key Principles
- Zod for validation: Base schemas validate core fields (no relationships)
- TypeScript for relationships: Interfaces extend base types with optional navigation properties
- Foreign keys: End with
Id suffix (e.g., submittedById, siteId)
- Navigation properties: Use full entity names (e.g.,
submitter, site, not submittedBy)
- Circular references: Use
import() types in TypeScript interfaces
When to Use This Skill
- Adding a new entity (e.g., File, Comment, Report)
- Adding fields to an existing entity
- Creating or modifying relationships
- Changing enums or validation rules
- Renaming fields or entities
Adding a New Entity
Step 1: Create the Entity File
Create model/YourEntity.ts:
import { z } from 'zod';
import { YourStatus } from './enums';
export const YourStatusSchema = z.nativeEnum(YourStatus);
export const YourEntityBaseSchema = z.object({
id: z.string(),
name: z.string(),
description: z.string(),
status: YourStatusSchema,
relatedEntityId: z.string(),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
});
export const YourEntitySchema = YourEntityBaseSchema;
export type YourEntityBase = z.infer<typeof YourEntityBaseSchema>;
export interface YourEntity extends YourEntityBase {
relatedEntity?: import('./RelatedEntity').RelatedEntity;
childEntities?: Array<import('./ChildEntity').ChildEntity>;
}
Step 2: Add Enum (if needed)
Add to model/enums.ts:
export enum YourStatus {
ACTIVE = 'active',
INACTIVE = 'inactive',
PENDING = 'pending',
}
Step 3: Update model/index.ts
Add exports:
export {
YourEntitySchema,
YourEntityBaseSchema,
YourStatusSchema,
type YourEntity,
type YourEntityBase,
} from './YourEntity';
Step 4: Update Related Entities
Add navigation properties to related entities:
export interface RelatedEntity extends RelatedEntityBase {
yourEntities?: Array<import('./YourEntity').YourEntity>;
}
Step 5: Update model/README.md
Update the Mermaid diagram to include the new entity and its relationships:
# In model/README.md, add to the erDiagram:
YourEntity }o--|| RelatedEntity : "belongs to"
YourEntity ||--o{ ChildEntity : "contains"
RelatedEntity ||--o{ YourEntity : "has"
ChildEntity }o--|| YourEntity : "belongs to"
Adding a Field to an Existing Entity
Step 1: Update Base Schema
Add field to the Zod schema:
export const JobBaseSchema = z.object({
priority: z.enum(['low', 'medium', 'high']),
});
Step 2: TypeScript Types Auto-Update
TypeScript types are inferred from the schema, so they update automatically.
Step 3: Update Mock Data
Update MSW handlers in mock-app/src/mocks/handlers.ts:
const mockJobs: Job[] = [
{
id: '1',
title: 'Site Survey',
priority: 'high',
},
];
Step 4: Update Storybook Stories
Update story data in *.stories.tsx files:
const exampleJob: Job = {
id: '1',
title: 'Site Survey',
priority: 'medium',
};
Changing a Relationship
- Remove relationship lines from
model/README.md Mermaid diagram
Adding a Relationship
- Add foreign key to base schema (e.g.,
ownerId)
- Add navigation property to TypeScript interface (e.g.,
owner?: User)
- Update related entity with inverse navigation property
- Update index.ts if new entities involved
Removing a Relationship
- Remove foreign key from base schema
- Remove navigation property from interface
- Remove inverse navigation property from related entity
- Update all mock data removing the field
Field Naming Conventions
| Type | Convention | Example |
|---|
| Foreign Key | entityNameId | userId, siteId, submittedById |
| Navigation Property (1:1 or many:1) | entityName | user, site, submitter |
| Navigation Property (1:many) | entityNames (plural) | jobs, submittedJobs |
| Enum | PascalCase | UserRole, JobStatus |
| Enum Values | SCREAMING_SNAKE_CASE | IN_PROGRESS, ACTIVE |
Common Validation Patterns
email: z.string().email()
url: z.string().url()
name: z.string().min(3).max(100)
priority: z.number().min(1).max(10)
isActive: z.boolean().default(true)
status: z.enum(['draft', 'published', 'archived'])
status: z.nativeEnum(JobStatus)
createdAt: z.string().datetime()
tags: z.array(z.string())
metadata: z.object({
version: z.string(),
source: z.string(),
})
Importing and Using the Model
In Components
import { Job, JobStatus } from '@model';
function JobCard({ job }: { job: Job }) {
console.log(job.title, job.submittedById);
if (job.submitter) {
console.log(job.submitter.name);
}
}
In MSW Handlers
import { Job, JobSchema } from '@model';
export const handlers = [
http.get('/api/jobs/:id', () => {
const job: Job = {
id: '1',
title: 'Site Survey',
};
return HttpResponse.json(job);
}),
];
Runtime Validation
import { JobSchema } from '@model';
try {
const validJob = JobSchema.parse(unknownData);
} catch (error) {
console.error('Validation failed:', error);
}
const result = JobSchema.safeParse(unknownData);
if (result.success) {
console.log(result.data);
} else {
console.error(result.error);
}
Common Mistakes to Avoid
❌ Don't: Mix FK and Navigation Property Names
export const JobBaseSchema = z.object({
submittedBy: z.string(),
});
export interface Job extends JobBase {
submittedBy?: User;
}
export const JobBaseSchema = z.object({
submittedById: z.string(),
});
export interface Job extends JobBase {
submitter?: User;
}
❌ Don't: Add Relationships to Zod Schemas
export const UserSchema = UserBaseSchema.extend({
submittedJobs: z.array(JobSchema),
});
export const UserSchema = UserBaseSchema;
export interface User extends UserBase {
submittedJobs?: Array<import('./Job').Job>;
}
❌ Don't: Forget to Update index.ts
When adding a new entity, always update model/index.ts to export it.
❌ Don't: Forget to Update Mock Data
After schema changes, update:
- MSW handlers (
mock-app/src/mocks/handlers.ts)
- Storybook stories (
*.stories.tsx)
- Test fixtures
Testing Schema Changes
After making changes, verify:
- TypeScript compiles: Check for errors in VS Code
- Mock data validates: Run dev server (
npm run dev)
- Stories render: Run Storybook (
npm run storybook)
- Tests pass: Run tests if available
Example: Adding a Priority Field
export enum JobPriority {
LOW = 'low',
MEDIUM = 'medium',
HIGH = 'high',
URGENT = 'urgent',
}
import { JobStatus, JobPriority } from './enums';
export const JobPrioritySchema = z.nativeEnum(JobPriority);
export const JobBaseSchema = z.object({
id: z.string(),
title: z.string(),
priority: JobPrioritySchema,
status: JobStatusSchema,
});
export {
JobSchema,
JobBaseSchema,
JobStatusSchema,
JobPrioritySchema,
type Job,
type JobBase,
} from './Job';
const mockJobs: Job[] = [
{
id: '1',
priority: JobPriority.HIGH,
},
];
Related Files
After updating the model, you may need to update:
del/README.md` - ALWAYS update the Mermaid diagram when adding/removing entities or relationships
- `mo
mock-app/src/mocks/handlers.ts - MSW mock data
mock-app/src/features/*/*.stories.tsx - Storybook stories
- Component prop types - If components accept entity types
- API integration code - When backend syncs with model
Questions to Ask Before Changing the Model
- Is this a breaking change? Will existing mock data still work?
- Do relationships make sense? Are they bidirectional where needed?
- Are FK names clear? Do they end with
Id?
- Is validation appropriate? Not too strict, not too loose?
- Are enums needed? Or should it be a string field?
Summary
- One entity per file in
model/
- Zod schemas for base fields only
- TypeScript interfaces for relationships
- FK fields end with
Id, navigation properties don't
- Always update
model/index.ts
- Update mock data and stories after changes