Skip to content

Changelog - 2026-01-02

Default Filter & Repository Mixins

This release introduces Default Filter - a powerful feature that automatically applies predefined filter conditions to all repository queries. Additionally, the repository architecture has been refactored to use composable mixins for better code organization and reusability.

NOTE

2026-08-01 Update: The import { FilterBuilder } from '@venizia/ignis' form shown below no longer resolves. The Postgres class is now PostgresFilterBuilder, from the same path; the name FilterBuilder belongs to the neutral abstract base at @venizia/ignis/relational. See Relational Connector Lift.

Overview

  • Default Filter: Configure automatic filter conditions at the model level (e.g., soft delete, tenant isolation)
  • Skip Default Filter: Bypass default filters with shouldSkipDefaultFilter: true for admin/maintenance operations
  • Repository Mixins: Extracted DefaultFilterMixin and FieldsVisibilityMixin for composable repository features
  • FilterBuilder Enhancement: Renamed DrizzleFilterBuilder to FilterBuilder, added mergeFilter method
  • IExtraOptions Interface: New interface replacing TTransactionOption with shouldSkipDefaultFilter support

New Features

Default Filter

Files:

  • packages/core-server/src/base/repositories/mixins/default-filter.ts
  • packages/core-server/src/base/repositories/operators/filter.ts

Problem: Applications often need to apply the same filter conditions to every query - soft delete (isDeleted: false), tenant isolation (tenantId: 'xxx'), or active record patterns. Without a centralized solution, developers must manually add these conditions to every repository call.

Solution: Configure a defaultFilter in your model settings. The repository automatically merges this filter with user-provided filters for all read, update, and delete operations.

typescript
// Model configuration with default filter
@model({
  type: 'entity',
  settings: {
    // Automatically applied to all queries
    defaultFilter: {
      where: { isDeleted: false },
      limit: 100,  // Prevent unbounded queries
    },
  },
})
export class User extends BaseEntity<typeof User.schema> {
  static override schema = userTable;
}

Automatic Application:

typescript
// User query - default filter automatically merged
await userRepo.find({
  filter: { where: { status: 'active' } }
});
// Actual query: WHERE isDeleted = false AND status = 'active' LIMIT 100

// Default filter also applies to count, update, delete
await userRepo.count({ where: { role: 'admin' } });
// Actual query: WHERE isDeleted = false AND role = 'admin'

Filter Merge Strategy:

PropertyStrategy
whereDeep merge (user overrides matching keys)
limitUser replaces default (if provided)
offset/skipUser replaces default (if provided)
orderUser replaces default (if provided)
fieldsUser replaces default (if provided)
includeUser replaces default (if provided)
typescript
// Default: { where: { isDeleted: false, status: 'pending' }, limit: 100 }
// User: { where: { status: 'active' }, limit: 10 }
// Result: { where: { isDeleted: false, status: 'active' }, limit: 10 }

Skip Default Filter

Problem: Admin users or maintenance scripts sometimes need to query all records, including soft-deleted ones or records from all tenants.

Solution: Pass shouldSkipDefaultFilter: true in the options to bypass the default filter:

typescript
// Normal query - default filter applies
await repo.find({ filter: { where: { role: 'admin' } } });
// WHERE isDeleted = false AND role = 'admin'

// Admin query - bypass default filter
await repo.find({
  filter: { where: { role: 'admin' } },
  options: { shouldSkipDefaultFilter: true }
});
// WHERE role = 'admin' (includes deleted records)

// Works with all operations
await repo.count({ where: {}, options: { shouldSkipDefaultFilter: true } });
await repo.updateAll({
  where: { status: 'archived' },
  data: { isDeleted: true },
  options: { shouldSkipDefaultFilter: true }
});
await repo.deleteAll({
  where: { createdAt: { lt: '2020-01-01' } },
  options: { shouldSkipDefaultFilter: true, force: true }
});

Benefits:

  • Automatic soft-delete filtering without manual where additions
  • Multi-tenant isolation at the data layer
  • Consistent query behavior across the application
  • Easy bypass for admin/maintenance operations

Repository Mixins

Files:

  • packages/core-server/src/base/repositories/mixins/default-filter.ts
  • packages/core-server/src/base/repositories/mixins/fields-visibility.ts
  • packages/core-server/src/base/repositories/mixins/index.ts

Problem: Repository base classes were becoming monolithic with multiple concerns (hidden properties, default filters, transaction handling) mixed together.

Solution: Extract cross-cutting concerns into composable mixins:

typescript
// DefaultFilterMixin - Provides default filter functionality
export const DefaultFilterMixin = <T extends TMixinTarget<object>>(baseClass: T) => {
  abstract class Mixed extends baseClass {
    getDefaultFilter(): TFilter | undefined;
    hasDefaultFilter(): boolean;
    applyDefaultFilter(opts: { userFilter?: TFilter; shouldSkipDefaultFilter?: boolean }): TFilter;
  }
  return Mixed;
};

// FieldsVisibilityMixin - Provides hidden properties functionality
export const FieldsVisibilityMixin = <T extends TMixinTarget<object>>(baseClass: T) => {
  abstract class Mixed extends baseClass {
    get hiddenProperties(): Set<string>;
    get visibleProperties(): Record<string, any> | undefined;
    getHiddenProperties(): Set<string>;
    hasHiddenProperties(): boolean;
    getVisibleProperties(): Record<string, any> | undefined;
  }
  return Mixed;
};

Usage in AbstractRepository:

typescript
export abstract class AbstractRepository<...>
  extends DefaultFilterMixin(FieldsVisibilityMixin(BaseHelper))
  implements IPersistableRepository<...>
{
  // Mixins provide getDefaultFilter, applyDefaultFilter,
  // getHiddenProperties, getVisibleProperties, etc.
}

Benefits:

  • Separation of concerns
  • Reusable logic across different repository types
  • Easier testing of individual features
  • Clear dependency chain

IExtraOptions Interface

File: packages/core-server/src/base/repositories/common/types.ts

Problem: The TTransactionOption type only supported transaction option. With the new default filter feature, we need additional options.

Solution: Introduced IExtraOptions interface that extends beyond transactions:

typescript
// New interface
export interface IExtraOptions extends IWithTransaction {
  /**
   * If true, bypass the default filter configured in model settings.
   * Use this when you need to query all records regardless of default filter constraints.
   */
  shouldSkipDefaultFilter?: boolean;
}

// Base transaction interface
export interface IWithTransaction {
  transaction?: ITransaction;
}

// Deprecated alias for backward compatibility
/** @deprecated Use IExtraOptions instead */
export type TTransactionOption = IExtraOptions;

FilterBuilder Enhancements

File: packages/core-server/src/base/repositories/operators/filter.ts

Changes:

  • Renamed DrizzleFilterBuilder to FilterBuilder
  • Added mergeFilter method for combining default and user filters
  • Added resolveHiddenProperties and resolveRelations methods
  • Simplified build method signature (relations resolved internally)
typescript
// New mergeFilter method
const filterBuilder = new FilterBuilder();

const result = filterBuilder.mergeFilter({
  defaultFilter: { where: { isDeleted: false }, limit: 100 },
  userFilter: { where: { status: 'active' }, limit: 10 }
});
// Result: { where: { isDeleted: false, status: 'active' }, limit: 10 }

Internal Changes:

  • Relations now resolved internally via resolveRelations method
  • Hidden properties resolved via resolveHiddenProperties method
  • Simplified API for build method (no need to pass resolvers)

Files Changed

Core Package (packages/core-server)

FileChanges
src/base/repositories/mixins/default-filter.tsNEW - DefaultFilterMixin implementation
src/base/repositories/mixins/fields-visibility.tsNEW - FieldsVisibilityMixin implementation
src/base/repositories/mixins/index.tsNEW - Barrel export for mixins
src/base/repositories/common/types.tsAdded IExtraOptions, IWithTransaction interfaces
src/base/repositories/core/base.tsRefactored to use mixins, updated method signatures
src/base/repositories/core/readable.tsAdded applyDefaultFilter calls to read operations
src/base/repositories/core/persistable.tsAdded applyDefaultFilter calls to update/delete
src/base/repositories/core/default-crud.tsUpdated type parameters
src/base/repositories/operators/filter.tsRenamed to FilterBuilder, added mergeFilter
src/helpers/inversion/common/types.tsAdded defaultFilter to model settings type

Tests (packages/core-server/src/__tests__)

FileChanges
default-filter/default-filter.test.tsNEW - Comprehensive test suite (150+ test cases)

Examples (examples/vert)

FileChanges
src/services/tests/default-filter-test.service.tsNEW - Integration test service
src/services/tests/TEST_CASES.mdNEW - Test case documentation

Breaking Changes

DrizzleFilterBuilder Renamed to FilterBuilder

Before:

typescript
import { DrizzleFilterBuilder } from '@venizia/ignis';
const builder = new DrizzleFilterBuilder();

After:

typescript
import { FilterBuilder } from '@venizia/ignis';
const builder = new FilterBuilder();

FilterBuilder.build() Signature Changed

Before:

typescript
filterBuilder.build({
  tableName: 'users',
  schema: userSchema,
  relations: { posts: { ... } },
  filter: myFilter,
  relationResolver: (schema) => { ... },
  hiddenPropertiesResolver: (relationName) => { ... },
});

After:

typescript
// Relations and hidden properties resolved internally
filterBuilder.build({
  tableName: 'users',
  schema: userSchema,
  filter: myFilter,
});

TTransactionOption Deprecated

Before:

typescript
import { TTransactionOption } from '@venizia/ignis';

class MyRepo extends AbstractRepository<Schema, Data, Persist, TTransactionOption> {}

After:

typescript
import { IExtraOptions } from '@venizia/ignis';

class MyRepo extends AbstractRepository<Schema, Data, Persist, IExtraOptions> {}

Migration Guide

Step 1: Update FilterBuilder Usage

If you're using DrizzleFilterBuilder directly (rare), rename to FilterBuilder:

typescript
// Before
import { DrizzleFilterBuilder } from '@venizia/ignis';

// After
import { FilterBuilder } from '@venizia/ignis';

Step 2: Update Custom Repository Generic Types

If you have custom repositories with explicit generic types:

typescript
// Before
class MyRepo extends PersistableRepository<Schema, Data, Persist, TTransactionOption> {}

// After
class MyRepo extends PersistableRepository<Schema, Data, Persist, IExtraOptions> {}

Step 3: Add Default Filters to Models (Optional)

To enable automatic filtering, add defaultFilter to your model settings:

typescript
@model({
  type: 'entity',
  settings: {
    defaultFilter: {
      where: { isDeleted: false },
    },
  },
})
export class User extends BaseEntity<typeof User.schema> {
  static override schema = userTable;
}

Common Use Cases

Soft Delete Pattern

typescript
@model({
  type: 'entity',
  settings: {
    defaultFilter: {
      where: { deletedAt: null },  // or { isDeleted: false }
    },
  },
})
export class Post extends BaseEntity<typeof Post.schema> {}

// All queries automatically exclude deleted posts
await postRepo.find({ filter: { where: { published: true } } });
// WHERE deletedAt IS NULL AND published = true

// Restore deleted post (bypass default filter)
await postRepo.updateById({
  id: postId,
  data: { deletedAt: null },
  options: { shouldSkipDefaultFilter: true }
});

Multi-Tenant Isolation

typescript
// Note: tenantId would be injected per-request in real applications
@model({
  type: 'entity',
  settings: {
    defaultFilter: {
      where: { tenantId: 'current-tenant-id' },
    },
  },
})
export class Document extends BaseEntity<typeof Document.schema> {}

Active Records Only

typescript
@model({
  type: 'entity',
  settings: {
    defaultFilter: {
      where: {
        isActive: true,
        expiresAt: { gt: new Date().toISOString() },
      },
      limit: 50,  // Prevent unbounded queries
    },
  },
})
export class Subscription extends BaseEntity<typeof Subscription.schema> {}

Documentation