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: truefor admin/maintenance operations - Repository Mixins: Extracted
DefaultFilterMixinandFieldsVisibilityMixinfor composable repository features - FilterBuilder Enhancement: Renamed
DrizzleFilterBuildertoFilterBuilder, addedmergeFiltermethod - IExtraOptions Interface: New interface replacing
TTransactionOptionwithshouldSkipDefaultFiltersupport
New Features
Default Filter
Files:
packages/core-server/src/base/repositories/mixins/default-filter.tspackages/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.
// 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:
// 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:
| Property | Strategy |
|---|---|
where | Deep merge (user overrides matching keys) |
limit | User replaces default (if provided) |
offset/skip | User replaces default (if provided) |
order | User replaces default (if provided) |
fields | User replaces default (if provided) |
include | User replaces default (if provided) |
// 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:
// 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
whereadditions - 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.tspackages/core-server/src/base/repositories/mixins/fields-visibility.tspackages/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:
// 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:
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:
// 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
DrizzleFilterBuildertoFilterBuilder - Added
mergeFiltermethod for combining default and user filters - Added
resolveHiddenPropertiesandresolveRelationsmethods - Simplified
buildmethod signature (relations resolved internally)
// 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
resolveRelationsmethod - Hidden properties resolved via
resolveHiddenPropertiesmethod - Simplified API for
buildmethod (no need to pass resolvers)
Files Changed
Core Package (packages/core-server)
| File | Changes |
|---|---|
src/base/repositories/mixins/default-filter.ts | NEW - DefaultFilterMixin implementation |
src/base/repositories/mixins/fields-visibility.ts | NEW - FieldsVisibilityMixin implementation |
src/base/repositories/mixins/index.ts | NEW - Barrel export for mixins |
src/base/repositories/common/types.ts | Added IExtraOptions, IWithTransaction interfaces |
src/base/repositories/core/base.ts | Refactored to use mixins, updated method signatures |
src/base/repositories/core/readable.ts | Added applyDefaultFilter calls to read operations |
src/base/repositories/core/persistable.ts | Added applyDefaultFilter calls to update/delete |
src/base/repositories/core/default-crud.ts | Updated type parameters |
src/base/repositories/operators/filter.ts | Renamed to FilterBuilder, added mergeFilter |
src/helpers/inversion/common/types.ts | Added defaultFilter to model settings type |
Tests (packages/core-server/src/__tests__)
| File | Changes |
|---|---|
default-filter/default-filter.test.ts | NEW - Comprehensive test suite (150+ test cases) |
Examples (examples/vert)
| File | Changes |
|---|---|
src/services/tests/default-filter-test.service.ts | NEW - Integration test service |
src/services/tests/TEST_CASES.md | NEW - Test case documentation |
Breaking Changes
DrizzleFilterBuilder Renamed to FilterBuilder
Before:
import { DrizzleFilterBuilder } from '@venizia/ignis';
const builder = new DrizzleFilterBuilder();After:
import { FilterBuilder } from '@venizia/ignis';
const builder = new FilterBuilder();FilterBuilder.build() Signature Changed
Before:
filterBuilder.build({
tableName: 'users',
schema: userSchema,
relations: { posts: { ... } },
filter: myFilter,
relationResolver: (schema) => { ... },
hiddenPropertiesResolver: (relationName) => { ... },
});After:
// Relations and hidden properties resolved internally
filterBuilder.build({
tableName: 'users',
schema: userSchema,
filter: myFilter,
});TTransactionOption Deprecated
Before:
import { TTransactionOption } from '@venizia/ignis';
class MyRepo extends AbstractRepository<Schema, Data, Persist, TTransactionOption> {}After:
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:
// 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:
// 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:
@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
@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
// 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
@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
- Default Filter Guide - Full documentation
- Repository Mixins - Mixin architecture
- Advanced Repository Features - Updated with shouldSkipDefaultFilter