Skip to content

ORM API

Import path: @sdcorejs/nestjs/core

The ORM layer supplies TypeORM base classes, filter/paging contracts, metadata decorators and an optional history-recorder bridge. It does not register entities for you.

Complete example

ts
import { Controller, Injectable } from '@nestjs/common';
import { Column, DataSource, Entity } from 'typeorm';
import {
  BaseController,
  BaseEntity,
  BaseRepository,
  BaseService,
  type Dto,
  Schema,
  SchemaProp,
  Scoped,
  SearchableFields,
  WithAudit,
} from '@sdcorejs/nestjs/core';

@Entity('product')
@Schema({ name: 'Product' })
@SearchableFields({ exact: ['sku'], contain: ['name'], activeColumn: 'active' })
export class Product extends WithAudit(BaseEntity) {
  @Column()
  @Scoped()
  tenantCode!: string;

  @Column({ length: 64 })
  @SchemaProp({ label: 'SKU', required: true, unique: true })
  sku!: string;

  @Column()
  @SchemaProp({ label: 'Name', required: true })
  name!: string;

  @Column({ default: true })
  active!: boolean;
}

export interface ProductDto extends Dto {
  sku: string;
  name: string;
}

@Injectable()
export class ProductRepository extends BaseRepository<Product> {
  constructor(dataSource: DataSource) {
    super(Product, dataSource, { logHistory: true });
  }
}

@Injectable()
export class ProductService extends BaseService<Product, ProductDto> {
  constructor(repository: ProductRepository) {
    super(repository);
  }

  mapDTO(entity: Product | null | undefined): ProductDto | null | undefined {
    return entity && { id: entity.id, sku: entity.sku, name: entity.name, deletable: true };
  }
}

@Controller('products')
export class ProductController extends BaseController<Product, ProductDto> {
  constructor(service: ProductService) {
    super(service);
  }
}

Register Product in the application's TypeORM datasource and register the repository, service and controller as normal Nest providers.

Entity and metadata exports

ExportKindContract
BaseEntityabstract classUUID id primary column
ConstructortypeConstructor accepted by mixins
WithTimestampsmixinAdds createdAt, updatedAt, deletedAt
WithAuditmixinAdds timestamps plus createdBy, modifiedBy, creator, modifier
isAuditEnabledfunctionDetects WithAudit metadata through inheritance
UserSnapshottype{ id, username, fullName } JSONB snapshot shape
Scoped, ScopedOptions, ScopedColumnMetadatadecorator/typesMarks required or optional tenancy columns
getScopedColumns, getScopedColumnMetadatafunctionsReads inherited scope metadata
SearchableFields, SearchableFieldsConfigdecorator/typeDeclares exact/contain search columns and optional active flag
getSearchableConfigfunctionReads searchable-field metadata
Schema, SchemaOptionsdecorator/typeClass-level UI/schema metadata
SchemaProp, SchemaPropOptionsdecorator/typeProperty-level UI/schema metadata
getSchema, getSchemaPropsfunctionsReads inherited schema metadata
ClassReftypeAbstract or concrete class reference

WithAudit uses PostgreSQL jsonb for creator and modifier. DefaultAuditStrategy fills only the UUID actor fields; snapshot population belongs in an application strategy.

Repository and service exports

ExportKindPurpose
BaseRepository<T>, BaseRepositoryOptionsclass/typeScoped TypeORM reads, writes, transactions, audit and history hooks
IBaseRepository<T>interfaceMockable/injectable public repository contract
BaseRepositoryArgs<T>interfaceRelations, soft-deleted inclusion and application-owned predicates
InactiveMutationTransactionErrorclassSupplied runner has no active transaction
BaseService<T, TDto>, IBaseService<T, TDto>class/interfaceDTO mapping and mutation-policy layer
DtointerfaceRequires id; optional deletable and restorable flags
BaseController<T, TDto>abstract classMinimal search/paging/detail/delete REST surface
Filter, FilterHasData, FilterBetween, FilterNoData, FilterAndOrtypesShared recursive filter contracts
Operator, OperatorHasData, OperatorNoDatatypesShared filter operator unions
Order, PagingReq, PagingRes, QueryReq, NestedKeyOftypesShared query, sort and paging contracts
ApiErrorBody, ApiResponseEnvelopeinterfacesStandard error and response envelopes
apiError, ApiResponsevaluesEnvelope constructors

Repository API

ts
new BaseRepository<T>(target, dataSource, options?)

interface BaseRepositoryOptions {
  logHistory?: boolean;
  tenancyStrategy?: ITenancyStrategy;
  auditStrategy?: IAuditStrategy;
  contextService?: ContextService;
  historyRecorder?: IHistoryRecorder;
}
MemberSignature / behavior
MAX_PAGE_SIZEStatic hard cap: 200
paging(req: PagingReq<T>, args?: BaseRepositoryArgs<T>) => Promise<PagingRes<T>>
pagingDeletedSame signature; includes active and soft-deleted rows
all(filters?, args?) => Promise<T[]>; deliberately unbounded
search(keyword, filters?) => Promise<T[]>; UUID exact lookup or at most 20 configured matches
detail(id, args?) => Promise<T | null>; validates UUID; excludes soft-deleted by default
findByIds(ids, args?) => Promise<T[]>; deduplicated and tenancy-scoped
create(entity, queryRunner?) => Promise<T>
update(entityWithId, queryRunner?) => Promise<T>; scope columns are immutable
delete(idOrIds, queryRunner?) => Promise<boolean>; hard delete
softDelete(idOrIds, queryRunner?) => Promise<boolean>
restore(idOrIds, queryRunner?) => Promise<boolean>
import(entities, queryRunner?) => Promise<T[]>; inserts in chunks of 1,000
targetTypeORM EntityTarget<T> getter
unsafeRepositoryRaw TypeORM repository; bypasses tenancy and mutation guards
unsafeGetRepositoryRaw repository for an optional runner; same warning
unsafeCreateQueryRunnerRaw runner factory; caller owns lifecycle and safety

Mutations own a transaction unless supplied a caller-owned QueryRunner. A supplied runner must already have an active transaction or InactiveMutationTransactionError is thrown. Scoped batch mutations require every requested row to match; partial matches return the same not-found error.

BaseRepositoryArgs<T> accepts relations?: string[], withDeleted?: boolean, and parameterized andWheres?: { where; parameters? }[]. withDeleted: true means active and deleted rows; it is not a deleted-only filter. Nested relation paths are joined parent-first and each scoped relation receives its own tenancy predicate.

Pagination, filtering and sorting

PagingReq, PagingRes, QueryReq, Order, Filter, FilterHasData, FilterBetween, FilterNoData, FilterAndOr, Operator, OperatorHasData, OperatorNoData, and NestedKeyOf are type-only re-exports from @sdcorejs/utils/models.

ts
const page = await products.paging({
  pageNumber: 0, // zero-based: first page
  pageSize: 25,  // 1..200; default 10
  filters: [
    { field: 'active', operator: 'EQUAL', data: true },
    {
      operator: 'OR',
      data: [
        { field: 'name', operator: 'CONTAIN', data: 'phone' },
        { field: 'sku', operator: 'START_WITH', data: 'PH-' },
      ],
    },
  ],
  orders: [{ field: 'createdAt', direction: 'DESC' }],
});

Repository pagination is zero-based, even though the shared structural type does not enforce runtime semantics. Negative pages become 0; missing/non-positive sizes become 10; sizes above 200 are clamped. Supported operators are EQUAL, NOT_EQUAL, CONTAIN, NOT_CONTAIN, IN, NOT_IN, START_WITH, NOT_START_WITH, END_WITH, NOT_END_WITH, GREATER_THAN, LESS_THAN, GREATER_OR_EQUAL, LESS_OR_EQUAL, BETWEEN, NULL, NOT_NULL, AND, and OR.

Service and controller API

IBaseRepository<T> and IBaseService<T, TDto> expose the public contracts of the corresponding base classes for DI and mocks. Dto requires id; deletable and restorable are opt-in policy flags used by service mutation methods.

BaseService maps repository results through abstract mapDTO and exposes paging, pagingDeleted, all, search, detail, create, import, update, delete, softDelete, restore, and schema. Delete/restore methods load every requested ID in scope and act only on DTOs whose policy flag is true.

BaseController mounts only this minimal surface:

RouteMethodResult
/search?keyword=...POSTApiResponse.ok(service.search(keyword, bodyFilters))
/pagingPOSTApiResponse.ok(service.paging(body))
/:idGETApiResponse.ok(service.detail(id))
/:idDELETEHard delete, then { data: null }

It does not add permissions. Override methods or define explicit application controllers with AuthGuard and @HasPermission(...).

History bridge

ExportContract
HistoryActionType'CREATE' | 'UPDATE' | 'DELETE'
HistoryEntryTable path, row ID, before/after data, persisted resource scope and optional runner
IHistoryRecorderrecord(entry: HistoryEntry): Promise<void>
registerHistoryRecorderRegisters the process-wide recorder
MissingHistoryResourceScopeErrorScoped row could not provide complete trusted history scope

Set logHistory: true on a repository. ActionHistoryModule registers its service as the recorder by default and writes within the repository transaction. If no recorder is available, logging is a no-op. See Action history.

Other public types and helpers

ApiErrorBody, ApiResponseEnvelope, apiError, and ApiResponse define the standard response shape. See the package root.

PostgreSQL behavior

The base repository intentionally emits PostgreSQL syntax:

  • Text matching uses LOWER(UNACCENT(column::text)); enable the PostgreSQL unaccent extension.
  • JSON/JSONB nested filter paths use -> and ->> operators.
  • Sorts use NULLS FIRST for ascending and NULLS LAST for descending.
  • import() uses INSERT ... RETURNING *.
  • WithAudit and several feature entities use jsonb; UUIDs and timestamp-with-time-zone columns are also part of the built-in schemas.

Treat other database engines as unsupported unless your application has verified every generated query and substituted incompatible entity column types.

Errors and security notes

  • Invalid UUIDs, fields, sorts and relation paths use stable core.repository.* error codes.
  • Filter field names are restricted to safe path characters and values are parameterized; sorts and relations are metadata-validated. Do not expose arbitrary BaseRepositoryArgs.andWheres to clients.
  • all() is unbounded and is intentionally absent from BaseController; expose it only for known small datasets or add an application-specific limit.
  • unsafe* accessors bypass tenancy, authorization, affected-row checks and history. Restrict them to reviewed infrastructure code.

Released under the MIT License.