Create or modify domain entities in the core package of this monorepo. Use this skill whenever the user asks to add a new entity, update an existing entity, add properties or methods to an entity, or work on the entity/dto layer in the core package. Trigger when the user says things like "create a Foo entity", "add a field to Bar", "I need a new domain object", or "add entity X to core". Also trigger for DTO creation or modification.
This skill helps you create or modify domain entities following the hexagonal architecture conventions of this project. Scope: entity class + DTO interface only — persistence (repository, mapper, MongoDB document) is a separate concern handled elsewhere.
Read the existing codebase before generating code:
packages/core/src/entity/) and read a similar existing entity as a referencepackages/core/src/dto/) and read a similar DTOCLAUDE.md inside the entity directory — it may have project-specific conventionsIf the project is new or the entity/dto directories are empty, use the reference files in this skill's references/ directory as your canonical examples instead:
references/entity-example.ts — fully annotated entity covering every patternreferences/dto-example.ts — fully annotated DTO covering every patternreferences/entity-union.ts — entity union pattern: abstract base + multiple concrete subtypes discriminated by a literaltypereferences/dto-union.ts — DTO union pattern: base interface + concrete sub-interfaces each carrying a type literal discriminantThese reference files cover all the patterns described below. Read them before generating code on a fresh project — they contain inline comments explaining the why behind each convention.
For a new entity you'll create two files:
src/dto/IEntityName.ts — the public data contract (DTO interface)src/entity/EntityName.ts — the domain entity classThen export both from their respective index files.
For modifying an existing entity, read the files first, then apply changes consistently across both.
The DTO is the public serialization contract. It has no methods — pure native data types.
// src/dto/IEntityName.ts
export default interface IEntityName {
_id: string; // always string (serialized from ObjectId or UUID)
name: string;
description: string;
slug?: string;
deleted_at: string | null; // DateTime serialized as ISO string
// foreign keys end with _id: string
parent_id: string | null;
}
Rules:
snake_case_id: stringparent_id, elemento_id, etc.DateTime → string (ISO); nullable → string | null// src/entity/EntityName.ts
import { Entity, IEntity } from "@dav/lib";
import { DateTime } from "luxon";
import { ObjectId } from "mongodb"; // or UUID — check what the project uses
import { Result, Maybe } from "@dav/lib/monad";
import IEntityName from "~/dto/IEntityName.js";
type EntityNameProps = {
name: string;
description: string;
deleted_at: DateTime<true> | null;
// Use domain types, not primitives for complex concepts
// e.g. email: EmailAddress (not string)
};
export default class EntityName extends Entity<EntityNameProps, ObjectId> implements IEntity<ObjectId> {
constructor(props: EntityNameProps, id?: ObjectId) {
super(props, new ObjectId(id)); // always wrap in ID constructor
}
// Getters — one per prop, no setters unless business-justified
get name(): string { return this.props.name; }
get description(): string { return this.props.description; }
// Business methods — describe intent, not data access
// Return Result<T, Error> for fallible operations
// Return void for infallible mutations
someOperation(input: string): Result<void, Error> {
if (!input) return Result.err(new Error("Invalid input"));
this.props.name = input;
return Result.ok(undefined);
}
toDTO(): IEntityName {
return {
_id: this._id.toHexString(), // ObjectId → string
// _id: this._id.toString(), // UUID → string
name: this.props.name,
description: this.props.description,
deleted_at: this.deleted_at?.toISO() ?? null,
// nested entity: this.props.nested?.toDTO() ?? null
// array: this.props.items.map(i => i.toDTO())
};
}
static create(props: {
name?: string;
description?: string;
deleted_at?: DateTime<true> | null;
// all props optional — use ?? to apply defaults
}, id?: ObjectId) {
return new EntityName({
name: props.name ?? "",
description: props.description ?? "",
deleted_at: props.deleted_at ?? null,
}, id);
}
}
Key rules:
super(props, new ObjectId(id)) — wraps id even if already the right typestatic create()create() props are optional with ?? defaults, required fields are the ones strictly necessary (usually dictated by business rules, not technical ones)deleted_at pattern: DateTime<true> | null for soft deletesWhen extending a base entity class:
type EntityNameProps = BaseProps & { extraField: string; };
class EntityName extends BaseClass<EntityNameProps, ObjectId> {
override toDTO(): IEntityName {
return { ...super.toDTO(), extraField: this.props.extraField };
}
}
For the union + abstract base pattern (multiple concrete subtypes distinguished by a type literal), read references/entity-union.ts (entities) and references/dto-union.ts (DTOs). Both files use matching type names and discriminants so you can see the entity↔DTO correspondence directly.
For entities that track who changed what:
// In props:
created_at: DateTime;
updated_at: DateTime;
updated_by: Operator | null; // import the Operator entity
// In business methods that mutate state:
this.props.updated_at = DateTime.now();
this.props.updated_by = operator;
Prefer value objects for complex domain concepts rather than raw primitives. Check the project's src/value_object/ directory for what's available before defaulting to string. Common ones:
EmailAddressPasswordAdd to src/entity/index.ts:
export { default as EntityName } from "./EntityName.js";
Add to src/dto/index.ts:
export type { default as IEntityName } from "./IEntityName.js";
src/dto/IEntityName.ts with _id: stringsrc/entity/EntityName.ts with create() factorycreate() has optional props with ?? defaultstoDTO() maps every DTO field; ID converted to stringEntityNameProps typecreate() updated with optional prop + defaulttoDTO() updated to include new field