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
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
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
@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
@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
@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
ColumnOptions
| Field | Type | Purpose |
|---|---|---|
sqlName | string | Override the SQL column name (defaults to the property name) |
pgType | PgColumnType | Explicit storage type — canonical names only, and only when it differs from what the annotation infers (whitelist-enforced) |
length | number | varchar(N)/char(N) on string, vector(N) on Vector |
precision | number | For numeric/decimal |
scale | number | For numeric/decimal |
nullable | boolean | NOT NULL by default — declare nullable: true for nullable columns |
defaultSql | string | Raw SQL DEFAULT expression (e.g. now(), gen_random_uuid()) |
jsType | "instant" | "decimal" | "vector" | Runtime hydration marker — required on Decimal/Vector props |
castInJoin | string | Cast 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 annotation | PG type |
|---|---|
bigint | int8 |
number | float8 |
boolean | boolean |
string | text |
Date | timestamptz |
UUID | uuid |
ISODate / ISOTime | date / time |
Decimal | numeric |
Vector | vector (+ length → vector(N)) |
| object-shaped types | jsonb |
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
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:
| Member | String | Column |
|---|---|---|
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
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
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
@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
@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
@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
Code
@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
Code
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
Code
@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 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
| Interface | Fields | Meaning |
|---|---|---|
IExposableEntity | uuid: string | Entity has a public UUID safe to expose outside the system |
IDeletableEntity | deleted_at?: Date; deleted_by?: string | Entity supports soft delete |
IAuditableEntity | created_at, created_by, updated_at, updated_by, version | Entity has audit stamps + optimistic-lock version |
IClonableEntity | cloned_from?: string | Entity 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
| Composition | Columns added | Use when |
|---|---|---|
Auditable(EntityBase) | created_at, created_by, updated_at, updated_by, version | Every audited entity |
Deletable(EntityBase) | deleted_at, deleted_by | Soft-delete entities |
Deletable(Auditable(EntityBase)) | all seven | The common case |
EntityBase alone | none | Plain 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
A hard-delete audited entity simply drops Deletable:
Code
Metadata helpers (advanced)
The library exports a few metadata helpers for tooling that needs entity metadata without instantiating the class:
Code
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()+AuditPortsystem. - Optimistic locking — the version guard powered by
@AuditableField(VERSION). - Clone — the
@CloneField()+Repository.clone()flow.