Skip to content

Repositories

Repositories provide type-safe CRUD operations. Use @repository decorator with both model and dataSource for auto-discovery.

The simplest approach - everything is auto-resolved:

typescript
// src/repositories/configuration.repository.ts
import { Configuration } from '@/models/entities';
import { PostgresDataSource } from '@/datasources/postgres.datasource';
import { DefaultCRUDRepository, repository } from '@venizia/ignis';

@repository({
  model: Configuration,
  dataSource: PostgresDataSource,
})
export class ConfigurationRepository extends DefaultCRUDRepository<typeof Configuration.schema> {
  // No constructor needed!

  async findByCode(opts: { code: string }) {
    return this.findOne({ filter: { where: { code: opts.code } } });
  }

  async findByGroup(opts: { group: string }) {
    return this.find({ filter: { where: { group: opts.group } } });
  }
}

Pattern 2: Explicit @inject

When you need constructor control (e.g., read-only repository or additional dependencies):

typescript
// src/repositories/user.repository.ts
import { User } from '@/models/entities';
import { PostgresDataSource } from '@/datasources/postgres.datasource';
import { inject, ReadableRepository, repository } from '@venizia/ignis';
import { CacheService } from '@/services/cache.service';

@repository({ model: User, dataSource: PostgresDataSource })
export class UserRepository extends ReadableRepository<typeof User.schema> {
  constructor(
    // First parameter MUST be DataSource injection
    @inject({ key: 'datasources.PostgresDataSource' })
    dataSource: PostgresDataSource, // Must be concrete type, not 'any'

    // After first arg, you can inject any additional dependencies
    @inject({ key: 'some.cache' })
    private cache: SomeCache,
  ) {
    super(dataSource);
  }

  async findByRealm(opts: { realm: string }) {
    // Use injected dependencies
    const cached = await this.cache.get(`user:realm:${opts.realm}`);
    if (cached) {
      return cached;
    }

    return this.findOne({ filter: { where: { realm: opts.realm } } });
  }
}

Important:

  • First constructor parameter MUST be the DataSource injection
  • After the first argument, you can inject any additional dependencies you need
  • When @inject is at param index 0, auto-injection is skipped

Repository Hierarchy

AbstractRepository (base + mixins: FieldsVisibilityMixin + DefaultFilterMixin)

ReadableRepository (read-only: find, findOne, findById, count, existsWith)

PersistableRepository (+ create, updateById, updateAll)

DefaultCRUDRepository (+ deleteById, deleteAll) — recommended default

SoftDeletableRepository (overrides delete to set deletedAt timestamp)
TypeDescription
ReadableRepositoryRead-only operations. Write operations throw errors.
PersistableRepositoryRead + write operations (create, update). Extends ReadableRepository.
DefaultCRUDRepositoryFull CRUD including delete. Extends PersistableRepository. Recommended for most use cases.
SoftDeletableRepositoryExtends DefaultCRUDRepository. Overrides delete to set deletedAt timestamp instead of physically removing records.

Querying Data

For advanced filtering with operators like gt, lt, like, in, between, and more, see Filter System.

typescript
const repo = this.get<ConfigurationRepository>({ key: 'repositories.ConfigurationRepository' });

// Find multiple records
const configs = await repo.find({
  filter: {
    where: { group: 'SYSTEM' },
    limit: 10,
    order: ['createdAt DESC'],
  }
});

// Find one record
const config = await repo.findOne({
  filter: { where: { code: 'APP_NAME' } }
});

// Select specific fields (array format)
const configCodes = await repo.find({
  filter: {
    fields: ['id', 'code', 'group'],  // Only these fields returned
    limit: 100,
  }
});

// Order by JSON/JSONB nested fields
const sorted = await repo.find({
  filter: {
    order: ['metadata.priority DESC', 'createdAt ASC'],
  }
});

// Create a record
const newConfig = await repo.create({
  data: {
    code: 'NEW_SETTING',
    group: 'SYSTEM',
    description: 'A new setting',
  }
});

// Update by ID
await repo.updateById({
  id: 'uuid-here',
  data: { description: 'Updated description' }
});

// Delete by ID
await repo.deleteById({ id: 'uuid-here' });

Extra Options

All repository operations accept an options parameter with these fields:

OptionTypeDescription
transactionITransactionTransaction context for atomic operations
shouldReturnbooleanWhether to return created/updated data (default: true)
shouldQueryRangebooleanReturn { data, range: { total, skip, limit } } for pagination
shouldSkipDefaultFilterbooleanBypass the model's default filter (e.g., soft delete)
typescript
// Create without returning data (faster)
await repo.create({
  data: { code: 'SETTING', group: 'SYSTEM' },
  options: { shouldReturn: false },
});

// Bulk create multiple records
await repo.createAll({
  data: [
    { code: 'SETTING_A', group: 'SYSTEM' },
    { code: 'SETTING_B', group: 'SYSTEM' },
  ],
});

// Query with pagination range
const result = await repo.find({
  filter: { limit: 20, skip: 0 },
  options: { shouldQueryRange: true }
});
// result = { data: [...], range: { total: 150, skip: 0, limit: 20 } }

Querying with Relations

Use include to fetch related data. The relation name must match what you defined in static relations:

typescript
const configWithCreator = await repo.findOne({
  filter: {
    where: { code: 'APP_NAME' },
    include: [{ relation: 'creator' }],
  },
});

console.log('Created by:', configWithCreator.creator.name);

Registering Repositories

typescript
// src/application.ts
export class Application extends BaseApplication {
  preConfigure(): ValueOrPromise<void> {
    this.dataSource(PostgresDataSource);
    this.repository(UserRepository);
    this.repository(ConfigurationRepository);
  }
}

SoftDeletableRepository

For soft-delete patterns, use SoftDeletableRepository which overrides delete operations to set a deletedAt timestamp instead of physically removing records:

typescript
import { SoftDeletableRepository, repository, model, BaseEntity } from '@venizia/ignis';
import { pgTable, timestamp } from 'drizzle-orm/pg-core';

@model({
  type: 'entity',
  settings: {
    hiddenProperties: ['deletedAt'],
    defaultFilter: { where: { deletedAt: null } },
  },
})
export class Category extends BaseEntity<typeof Category.schema> {
  static override schema = pgTable('Category', {
    ...generateIdColumnDefs({ id: { dataType: 'string' } }),
    ...generateTzColumnDefs(),
    deletedAt: timestamp('deleted_at', { withTimezone: true }),
    name: text('name').notNull(),
  });
}

@repository({ dataSource: PostgresDataSource, model: Category })
export class CategoryRepository extends SoftDeletableRepository<typeof Category.schema> {}

Repository Template

typescript
import { DefaultCRUDRepository, repository } from '@venizia/ignis';
import { MyModel } from '@/models/entities';
import { PostgresDataSource } from '@/datasources/postgres.datasource';

@repository({ model: MyModel, dataSource: PostgresDataSource })
export class MyModelRepository extends DefaultCRUDRepository<typeof MyModel.schema> {}

Advanced Topics

Performance: Core API Optimization

Ignis automatically optimizes "flat" queries (no relations, no field selection) by using Drizzle's Core API. This provides ~15-20% faster queries for simple reads. The canUseCoreAPI() method on ReadableRepository determines when this optimization applies.

Modular Persistence with Components

Bundle related persistence resources into Components for better organization:

typescript
export class UserManagementComponent extends BaseComponent {
  override binding() {
    this._application.dataSource(PostgresDataSource);
    this._application.repository(UserRepository);
    this._application.repository(ProfileRepository);
  }
}

Deep Dive: See Repository Reference for filtering operators, relations, JSON path queries, and array operators.

See Also