PrimeBrickPrimeBrick
  • Docs
  • Contact
  • MIT License
  • Documentation
  • MCP Server
  • API Catalog
  • Services
  • Libraries
PrimeBrickPrimeBrick

© 2026 PrimeBrick. MIT License. v3.11.2

github
DAL Library
    Developer Guide
      Getting startedArchitectureEntities & decoratorsColumn typesQuery DSLRepositoryRepository PatternEntity Service FactoryConnections & transactionsAudit trailPartitioning & retentionOptimistic Locking & ConcurrencyCloneDatabase MigrationsChangelogAPI Reference
SDK Library
Decimal Library
powered by Zudoku
Developer Guide

Entities & decorators

Entities are plain TypeScript classes decorated with @Entity, @Column, @Key, @Unique, and the audit/delete/clone decorators. The class maps to a table; each decorated property maps to a column. snake_case is used everywhere — DB column, TS property, JSON response.

The minimum viable entity

Code
import "reflect-metadata"; import { Entity, Column, Key, Unique, type UUID } from "@primebrick/dal-pg"; import { Decimal } from "@primebrick/dal-pg"; @Entity("products") export class ProductEntity { @Key() id!: bigint; @Unique() uuid!: UUID; @Column({ nullable: false }) name!: string; @Column({ jsType: "decimal", precision: 18, scale: 2, nullable: false }) price!: Decimal; }

That's enough to use dal.findById, dal.findByUUID, dal.findAll, dal.add, dal.update, dal.delete, dal.hardDelete. Add the audit/delete decorators to opt into soft delete, audit trail, and optimistic locking.

Decorator reference

@Entity(tableName?, schema?)

Maps a class to a database table. If tableName is omitted, the table name defaults to the class name. If schema is set, it overrides the Dal gateway's default search_path schema for this entity only.

Code
@Entity("customers") // table "customers" in the Dal's primary schema (schemas[0]) @Entity("customers", "billing") // table "billing.customers" (schema override) @Entity() // table name === class name export class CustomerEntity { ... }

The schema pin (explicit or the schemas[0] default) must belong to the DalConfig.schemas domain — an entity pinned elsewhere throws SchemaDomainViolationError at getDal() (boot) and on every entity operation, before SQL is generated.

The table name is also exposed via Reflect.defineMetadata("primebrick:tableName", ...) so external tooling (e.g. the SDK's CacheKeyBuilder) can read it without importing @primebrick/dal-pg.

@Key()

Marks exactly one column as the primary key. Used by update/delete/ restore/hardDelete as the default matchBy column. Defaults to the identity generation strategy (DB auto-generates).

Code
@Key() id!: bigint;

@Unique()

Marks a column as having a unique index. Used by clone() to identify columns that must be excluded from the copy (a new value is generated for them). The canonical example is uuid:

Code
@Unique() uuid!: UUID;

@IsNotColumn()

Excludes a property from persistence metadata and DAL queries. Use it for computed/transient properties that live on the class but not in the table.

Code
@IsNotColumn() get displayLabel(): string { return `${this.name} <${this.email}>`; }

@Column(sqlName?: string | ColumnOptions)

Overrides the column's SQL name, PG type, nullability, length, precision, scale, default SQL expression, or join cast type. If you don't need any of those overrides, you can omit @Column() entirely — every own enumerable property of new ctor() is a column by convention (see Architecture: syncImplicitEntityColumns).

@Column() accepts either a string (treated as sqlName) or an options object. The options object must set at least one field — an empty @Column({}) throws TypeError.

Code
@Column("full_name") // rename column only @Column({ length: 255 }) // varchar(255) on a `string` prop @Column({ jsType: "decimal", precision: 18, scale: 2 }) // numeric(18,2) on a `Decimal` prop @Column({ nullable: true }) // nullable column @Column({ defaultSql: "gen_random_uuid()" }) // DB-side default @Column({ castInJoin: "uuid" }) // cast to uuid in JOIN ON

ColumnOptions

FieldTypePurpose
sqlNamestringOverride the SQL column name (defaults to the property name)
pgTypePgColumnTypeExplicit storage type — canonical names only, and only when it differs from what the annotation infers (whitelist-enforced)
lengthnumbervarchar(N)/char(N) on string, vector(N) on Vector
precisionnumberFor numeric/decimal
scalenumberFor numeric/decimal
nullablebooleanNOT NULL by default — declare nullable: true for nullable columns
defaultSqlstringRaw SQL DEFAULT expression (e.g. now(), gen_random_uuid())
jsType"instant" | "decimal" | "vector"Runtime hydration marker — required on Decimal/Vector props
castInJoinstringCast type to apply when this field is used in a JOIN ON clause

Type resolution

When pgType is omitted, the PG type resolves statically from the TS type annotation in the schema pipeline — never from the property name and never from runtime reflection. The full matrix and the authoring rules (guarded types, whitelist, warnings) live in Column types. The short version:

TS annotationPG type
bigintint8
numberfloat8
booleanboolean
stringtext
Datetimestamptz
UUIDuuid
ISODate / ISOTimedate / time
Decimalnumeric
Vectorvector (+ length → vector(N))
object-shaped typesjsonb

Nullability is NOT inferred: columns default to NOT NULL; a nullable column must declare @Column({ nullable: true }) (or a decorator that implies it — @DeletableField, @CloneField).

@AuditableField(type) — opt into audit columns

Marks a column as one of the five audit fields. The type argument can be the AuditableFieldType enum or the equivalent string literal.

Code
import { AuditableField, AuditableFieldType } from "@primebrick/dal-pg"; @AuditableField(AuditableFieldType.CREATED_AT) created_at!: Date; @AuditableField(AuditableFieldType.CREATED_BY) created_by!: string; @AuditableField(AuditableFieldType.UPDATED_AT) updated_at!: Date; @AuditableField(AuditableFieldType.UPDATED_BY) updated_by!: string; @AuditableField(AuditableFieldType.VERSION) version!: number; // Equivalent object form (accepted by the decorator): @AuditableField({ type: "createdAt" }) created_at!: Date;

The VERSION column is what powers optimistic locking — if the entity has a version column, the version guard is enforced on every write. There is no separate @OptimisticLock() flag.

AuditableFieldType enum values:

MemberStringColumn
CREATED_AT"CREATED_AT"created_at
CREATED_BY"CREATED_BY"created_by
UPDATED_AT"UPDATED_AT"updated_at
UPDATED_BY"UPDATED_BY"updated_by
VERSION"VERSION"version

@DeletableField(type) — opt into soft delete

Marks a column as one of the two soft-delete fields. Finders exclude soft-deleted rows by default (deletedRecords: "EXCLUDED"); pass "INCLUDED" or "ONLY" to override.

Code
import { DeletableField, DeletableFieldType } from "@primebrick/dal-pg"; @DeletableField(DeletableFieldType.DELETED_AT) deleted_at?: Date | null; @DeletableField(DeletableFieldType.DELETED_BY) deleted_by?: string | null; // Equivalent object form: @DeletableField({ type: "deletedAt" }) deleted_at?: Date | null;

DeletableFieldType enum values: DELETED_AT ("DELETED_AT"), DELETED_BY ("DELETED_BY").

@SynchronizableField(type) — external sync tracking

Marks a column as the last_synced_at field. Used by integrations that sync records from an external system on a schedule.

Code
import { SynchronizableField, SynchronizableFieldType } from "@primebrick/dal-pg"; @SynchronizableField(SynchronizableFieldType.LAST_SYNCED_AT) @Column({ nullable: true }) last_synced_at?: Date | null;

SynchronizableFieldType enum values: LAST_SYNCED_AT ("LAST_SYNCED_AT").

@CloneField() — clone tracking

Marks a column as the clone-tracking field. Repository.clone() stamps it with the source record's UUID so the clone knows its origin. See Clone.

Code
@CloneField() cloned_from?: UUID | null;

@AuditTrail() — entity HAS an audit trail table

Marks an entity as having a companion audit table (e.g. customers → customers_audit). When @AuditTrail() is present and an AuditPort is injected, write operations automatically compute a field-level delta and call audit.writeAudit() fire-and-forget. See Audit trail.

Code
@Entity("customers") @AuditTrail() export class CustomerEntity { ... }

@AuditTrail() is also what enables optimistic locking — the version column guard is applied to every entity that has @AuditTrail() (because auditable entities always have a VERSION column).

@AuditTrailEntity(options?) — entity IS an audit trail table

Marks a class as being an audit trail table (a read-only view over an audit table like customers_audit). Records the changed_by column name so that buildAuditTrailJoins() can resolve it. See Audit trail.

Code
import { Entity, Column, Key, AuditTrailEntity } from "@primebrick/dal-pg"; @Entity("audit_log") @AuditTrailEntity({ changedByColumn: "changed_by" }) export class AuditLogEntity { @Key() id!: bigint; @Column({ nullable: false }) entity_id!: bigint; @Column({ nullable: false }) entity_uuid!: UUID; @Column({ nullable: false }) action!: string; @Column({ nullable: false }) changed_at!: Date; @Column({ nullable: false }) changed_by!: string; @AuditableField(AuditableFieldType.VERSION) version!: number; @Column({ nullable: true }) delta?: Record<string, { old: unknown; new: unknown }>; }

@Index(opts?) — secondary index

Declares a secondary (non-unique) index. Bare @Index() → single-column btree auto-named <table>_<column>_idx. Columns sharing the same name form a composite index ordered by order. Unique indexes stay on @Unique().

Code
export type IndexOptions = { name?: string; // shared name → composite index order?: number; // position inside the composite (default 0) using?: IndexMethod; // access method — default btree opclass?: string; // e.g. "vector_cosine_ops" (hnsw), "gin_trgm_ops" (gin) where?: string; // partial-index predicate, e.g. "deleted_at IS NULL" };
Code
@Column({ nullable: false }) @Index({ using: "hnsw", opclass: "vector_cosine_ops" }) embedding!: number[]; @Column() @Index({ where: "deleted_at IS NULL" }) email!: string;

@ForeignKey(target, opts?) — REFERENCES constraint

Declares a FOREIGN KEY referencing another entity's column. target is a lazy thunk (() => CustomerEntity) so circular references resolve at meta-read time, not class-eval time. The zero-option form references the target's @Key() column with ON DELETE RESTRICT / ON UPDATE NO ACTION — the safest default.

Code
export type ForeignKeyOptions = { column?: string; // referenced column — default: target's @Key() onDelete?: ForeignKeyAction; // default "restrict" onUpdate?: ForeignKeyAction; // default "no_action" name?: string; // constraint name — default <table>_<column>_fkey };
Code
@Column({ nullable: false }) @ForeignKey(() => CustomerEntity, { onDelete: "cascade" }) customer_uuid!: UUID;

A @PartitionField-partitioned parent cannot be a plain FK target (unique constraints must contain the partition key) — meta validation throws and points to @PartitionChildOf.

@PartitionField(opts?) — RANGE partition key

Marks a column as the RANGE partition key (pg_partman, daily/monthly/…). Exactly ONE per entity; the column must be date/timestamptz-typed and is forced NOT NULL (a NULL would fall into no partition). Every unique group and the primary key emitted in DDL must include it — meta validation throws otherwise, so a partitioned entity can never produce an invalid table definition.

Code
export type PartitionFieldOptions = { interval?: string; // pg_partman interval — default "1 month" retention?: string; // e.g. "6 months" — undefined = no auto-retention keepTable?: boolean; // retention_keep_table — false = DROP (default) premake?: number; // future partitions kept ahead };
Code
@Column({ nullable: false }) @PartitionField({ interval: "1 month", retention: "6 months" }) last_seen_at!: Date;

@PartitionChildOf(opts) — membership in a partition unit (class decorator)

Declares the entity a logical child of a @PartitionField-partitioned parent: it joins the parent's atomic delete and retention purge unit. Needed because a partitioned parent cannot host a plain FOREIGN KEY — the relationship is application-level, enforced by the repository cascade and the partition-maintenance purge. Entities that merely reference a parent (telemetry, logs) don't declare this.

Code
@Entity("ai_messages") @PartitionChildOf({ parent: AiConversationEntity, ref: "conversation_uuid" }) export class AiMessageEntity { ... }

Entity interfaces

The library ships four marker interfaces that document an entity's capabilities. They are types only — they have no runtime effect — but they make the entity's contract explicit and let consumers narrow on them.

Code
import type { IExposableEntity, IDeletableEntity, IAuditableEntity, IClonableEntity, } from "@primebrick/dal-pg";
InterfaceFieldsMeaning
IExposableEntityuuid: stringEntity has a public UUID safe to expose outside the system
IDeletableEntitydeleted_at?: Date; deleted_by?: stringEntity supports soft delete
IAuditableEntitycreated_at, created_by, updated_at, updated_by, versionEntity has audit stamps + optimistic-lock version
IClonableEntitycloned_from?: stringEntity supports cloning

IAuditableEntity and IDeletableEntity are independent capabilities — an entity can be audited but hard-deleted (api_keys), deletable but not audited, both, or neither. Compose them with & where a signature needs both (the Dal write-path already does: TEntity extends IAuditableEntity & IDeletableEntity for delete/restore).

Capability mixins — declare shared columns once

@AuditableField and @DeletableField are the underlying machinery, but standard entities never apply them by hand — the same five audit columns and two delete markers would be copy-pasted into every entity. The library exports mixin factories that own the field set in a single declaration:

Code
import { EntityBase, Auditable, Deletable, } from "@primebrick/dal-pg";
CompositionColumns addedUse when
Auditable(EntityBase)created_at, created_by, updated_at, updated_by, versionEvery audited entity
Deletable(EntityBase)deleted_at, deleted_bySoft-delete entities
Deletable(Auditable(EntityBase))all sevenThe common case
EntityBase alonenonePlain tables (logs, telemetry)

Each mixin is a real class in the prototype chain — the persistence layer merges decorated columns from every ancestor, so the subclass only declares its domain columns. Extra type-level capabilities keep their implements on the concrete class (IExposableEntity, IClonableEntity, …).

A complete entity

Putting it all together — an entity with PK, UUID, business fields, audit columns, soft delete, and clone tracking:

Code
import "reflect-metadata"; import { Entity, Column, Key, Unique, CloneField, AuditTrail, EntityBase, Auditable, Deletable, } from "@primebrick/dal-pg"; import type { IClonableEntity } from "@primebrick/dal-pg"; @Entity("customers") @AuditTrail() export class CustomerEntity extends Deletable(Auditable(EntityBase)) implements IClonableEntity { @Key() id!: bigint; @Unique() uuid!: UUID; @Column({ nullable: false }) name!: string; @Column({ nullable: false }) email!: string; @Column({ nullable: false, defaultSql: "false" }) is_active!: boolean; @CloneField() cloned_from?: UUID | null; }

A hard-delete audited entity simply drops Deletable:

Code
@Entity("api_keys") export class ApiKeyEntity extends Auditable(EntityBase) { @Key() id!: bigint; @Unique() uuid!: UUID; @Column({ nullable: false }) key_hash!: string; }

Metadata helpers (advanced)

The library exports a few metadata helpers for tooling that needs entity metadata without instantiating the class:

Code
import { isEntityClass, getTableName, getQualifiedTableName, getEntityName, getColumnName, getPrimaryKeyColumn, getEntityPersistenceMeta, listEntityPersistencePropertyKeys, syncImplicitEntityColumns, } from "@primebrick/dal-pg"; isEntityClass(CustomerEntity); // true (has @Entity) getTableName(CustomerEntity); // "customers" getEntityName(CustomerEntity); // "CustomerEntity" getColumnName(CustomerEntity, "created_at"); // "created_at" getPrimaryKeyColumn(CustomerEntity); // { sqlName, propertyKey } for @Key() getEntityPersistenceMeta(CustomerEntity); // full metadata object listEntityPersistencePropertyKeys(CustomerEntity); // ["id", "uuid", "name", ...] syncImplicitEntityColumns(CustomerEntity); // walk properties + infer nullability

syncImplicitEntityColumns is called automatically by the Repository on first use of an entity; you only need to call it manually if you're reading metadata before any Repository call.

Next steps

  • Query DSL — build type-safe filters, sorts, joins, projections.
  • Repository — finders, writes, bulk ops, streaming.
  • Audit trail — the @AuditTrail() + AuditPort system.
  • Optimistic locking — the version guard powered by @AuditableField(VERSION).
  • Clone — the @CloneField() + Repository.clone() flow.
Last modified on October 28, 2026
ArchitectureColumn types
On this page
  • The minimum viable entity
  • Decorator reference
    • @Entity(tableName?, schema?)
    • @Key()
    • @Unique()
    • @IsNotColumn()
    • @Column(sqlName?: string | ColumnOptions)
    • @AuditableField(type) — opt into audit columns
    • @DeletableField(type) — opt into soft delete
    • @SynchronizableField(type) — external sync tracking
    • @CloneField() — clone tracking
    • @AuditTrail() — entity HAS an audit trail table
    • @AuditTrailEntity(options?) — entity IS an audit trail table
    • @Index(opts?) — secondary index
    • @ForeignKey(target, opts?) — REFERENCES constraint
    • @PartitionField(opts?) — RANGE partition key
    • @PartitionChildOf(opts) — membership in a partition unit (class decorator)
  • Entity interfaces
  • Capability mixins — declare shared columns once
  • A complete entity
  • Metadata helpers (advanced)
  • Next steps
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript