packages/wrap/src/dto.ts and packages/wrap/src/entity.ts. Zod is the single source of truth for validation, typing, and OpenAPI generation on the DTO side; Drizzle tables are the single source of truth on the entity side.

Entity(name, columns, options?)

function Entity<TName extends string, TColumns extends Record<string, PgColumnBuilderBase>, TRel = {}>(
  name: TName,
  columns: TColumns,
  options?: { relations?: (helpers: EntityRelationBuilders<Tb>) => TRel },
): EntityClass<Tb, TRel>

Builds a Drizzle pgTable(name, { ...BaseRow, ...columns }) and wraps it in a class — the data-mapper vehicle repositories key off, not an ORM active-record. BaseRow (id/createdAt/updatedAt/deletedAt) is merged in automatically; every entity has it.

export class Example extends Entity("examples", {
  name: text("name").notNull(),
}, {
  relations: ({ many }) => ({ items: many(() => ExampleItem) }),
}) {}

// drizzle-kit + populate typing need these two named exports:
export const ExampleTable = Example.table;
export const ExampleRelations = relationsOf(Example);
type EntityClass<Tb, TRel> = {
  new (): InferSelectModel<Tb>;       // the typed row shape — never actually instantiated
  readonly table: Tb;
  readonly tableName: Tb["_"]["name"];
  readonly relationsConfig?: (helpers: EntityRelationBuilders<Tb>) => TRel;
};

BaseRow

const BaseRow = {
  id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
  createdAt: timestamp("created_at").defaultNow(),
  updatedAt: timestamp("updated_at").$onUpdate(() => new Date()),
  deletedAt: timestamp("deleted_at"),
};

updatedAt’s $onUpdate is what makes offline-sync tombstones work: setting deletedAt on a soft-delete also bumps updatedAt, so a client’s next findChangedSince pull sees the row without any extra bookkeeping.

relationsOf(entity, config?)

function relationsOf<Tb, TRel>(entity: EntityClass<Tb, TRel>): Relations<...>;
function relationsOf<Tb, TRel>(entity: EntityClass<Tb, any>, config: (helpers) => TRel): Relations<...>;

Builds the Drizzle relations() value from the entity’s own relationsConfig (declared via Entity(..., { relations })), or from an explicit config argument — use the explicit form when two entities reference each other, since mutual references inside extends Entity(...) type clauses would be a TypeScript type cycle. The callback is evaluated lazily by Drizzle at schema-build time, after every entity module has loaded, so forward references (() => OtherEntity) are safe.

interface EntityRelationBuilders<Tb> {
  self: Tb; // this entity's own table, for referencing FK columns
  one<TE extends EntityLike>(target: () => TE, config?: { fields: PgColumn[]; references?: (table) => PgColumn[] }): One;
  many<TE extends EntityLike>(target: () => TE): Many;
}

references defaults to the target’s id column when omitted. Targets are always referenced through a thunk (() => TargetEntity) so relations can point at entities in other feature slices without import-order issues.

tableOf(source)

function tableOf<S extends PgTable | EntityLike>(source: S): S extends EntityLike ? S["table"] : S;

Resolves a raw PgTable from either an entity class or an already-raw table — used internally by the DTO factories below and by anything else that accepts DTOSource.

DTOs

BaseDTO (abstract)

abstract class BaseDTO {
  static schema: z.ZodObject<any>; // override per subclass — the single source of truth

  static from<T>(this: new () => T, plain: unknown): T;       // parses (throws ZodError on failure), strips unknown keys
  static fromStrict<T>(this: new () => T, plain: unknown): T;  // @deprecated — from() already strips by default
  static safeFrom<T>(this: new () => T, plain: unknown):
    | { success: true; data: T }
    | { success: false; error: z.ZodError };

  validate(): z.core.$ZodIssue[]; // validate the current instance; [] means valid
}

Every DTO in the framework — hand-written or generated by the factories below — is ultimately a BaseDTO subclass with a schema. @ValidateDTO() (Decorators) and @Serialize() both key off schema directly.

SchemaDTO(schema, name?)

function SchemaDTO<S extends z.ZodObject<any>>(schema: S, name?: string): DTOClass<z.infer<S>>

Wrap an arbitrary Zod object schema as a DTO class — the escape hatch for a DTO that isn’t table-derived (a login payload, a webhook body, …):

class LoginDTO extends SchemaDTO(z.object({ email: z.string().email(), password: z.string().min(8) })) {}

Passing name also registers it in DTO_CLASSES under that name (equivalent to @DTO(), useful when the class isn’t declared with an explicit class X extends SchemaDTO(...) statement that a decorator could attach to).

SelectDTO(source, options?)

function SelectDTO<Src extends DTOSource>(
  source: Src,                     // an Entity class or a raw PgTable
  options?: { exclude?: string[] },
): DTOClass<Omit<InferSelectModel<Table>, K>>

Derives a DTO from the table’s SELECT shape via drizzle-zod’s createSelectSchema. Registered by table name ("<table>Base", or "<table>Base_excluded_..." when exclude is given) — calling it twice for the same table/exclude combination returns the same cached class.

export const ExampleBase = SelectDTO(Example, { exclude: ["deletedAt"] });

InsertDTO(source, options?)

function InsertDTO<Src extends DTOSource>(
  source: Src,
  options?: { exclude?: string[] },
): DTOClass<Omit<InferInsertModel<Table>, K | "id" | "createdAt" | "updatedAt" | "deletedAt">>

Same mechanism, derived from the INSERT shape (createInsertSchema). id/createdAt/updatedAt/deletedAt are excluded automatically — a create payload never carries them — on top of whatever options.exclude adds.

export class CreateExampleDTO extends InsertDTO(Example) {}

PartialDTO(DTOClassRef)

function PartialDTO<T>(DTOClassRef: DTOClass<T>): DTOClass<Partial<T>>

All fields optional — the standard shape for a PATCH/update DTO:

export class UpdateExampleDTO extends PartialDTO(CreateExampleDTO) {}

PaginatedResponseDTO(ItemClass)

function PaginatedResponseDTO<T>(ItemClass: DTOClass<T>): DTOClass<PaginatedResponse<T>>

Wraps an item DTO in the standard pagination envelope ({ items, itemCount, page, pageSize, pageCount }) — used to document a findPaginated endpoint’s response shape in Swagger.

Pre-built DTOs

Exported ready to use — every one is already @DTO()-registered:

DTO Shape
PaginationQuerysDTO { page?, pageSize?, search?, sortBy?, sortOrder?, includeDeleted?, populateChildren?, filters? } — matches PaginationQuery.
BaseErrorDTO { message, success?, details? }
BaseDeletedSuccessDTO { deleted: boolean, id: string }
DeleteMultipleDTO { ids: string[] }
BaseDeleteMultipleSuccessDTO { deleted, deletedCount, requestedCount, message? }
StatisticsPeriodDTO { count, period, percentage? }
StatisticsComparisonDTO { current: StatisticsPeriod, previous: StatisticsPeriod, growth, growthPercentage }
EntityStatisticsDTO { monthly, weekly, yearly } (each a StatisticsComparison) — matches getStatistics() on repositories/services.

toSerializationSchema(schema)

function toSerializationSchema(schema: z.ZodObject<any>): z.ZodObject<any>

Derives a schema’s output shape for response serialization: identical field-by-field, except every date field accepts Date | string and transforms to an ISO string on parse. Used internally by @Serialize / serialize() so a fresh entity (real Date objects) and an already-serialized payload (ISO strings, e.g. round-tripped through a queue or cache) both parse the same way. Cached per input schema (WeakMap) — cheap to call repeatedly.


This site uses Just the Docs, a documentation theme for Jekyll.