Asena Drizzle
Drizzle ORM utilities for AsenaJS - A powerful and type-safe database integration package that provides generic Database services and Repository patterns.
Features
- 🚀 Generic Database Service - Support for multiple database types
- 🎯 Type-Safe Repository Pattern - Full TypeScript type inference
- 🏷️ Decorator-Based Configuration - Easy setup with
@Database,@Repository,@Transaction, and@Drizzle - 🔄 Declarative Transactions -
@TransactionwithREQUIRED/NESTED/REQUIRES_NEWpropagation, propagated throughAsyncLocalStorage - 🔧 AsenaJS Integration - Seamless IoC container integration
- 📦 Multiple Database Support - Connect to different databases simultaneously
- ⚡ Performance Optimized - Configurable connection pooling and efficient queries
- 🔌 Symmetric Connection Lifecycle - the pool is opened by
@OnStartand released by@OnStop
Installation
# Core packages
bun add @asenajs/asena-drizzle drizzle-orm
# Database drivers
bun add pg # For PostgreSQL
bun add mysql2 # For MySQLRequirements:
- Bun v1.3.12 or higher
- @asenajs/asena v0.10.0 or higher
- drizzle-orm v0.45.2 or higher
Supported Databases
- ✅ PostgreSQL - using
pg(node-postgres) with connection pooling - ✅ MySQL - using
mysql2 - ✅ BunSQL - using Bun's built-in SQL interface
- ⏳ SQLite - coming soon
Quick Start
1. Define Your Schema
import { pgTable, uuid, text, timestamp, boolean } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
isActive: boolean('is_active').default(true),
createdAt: timestamp('created_at').defaultNow()
});2. Setup Database Service
// database/schemas/index.ts
import * as User from '../schemas/user.schema.ts';
export default {
...User,
};import { Database, AsenaDatabaseService } from '@asenajs/asena-drizzle';
@Database({
type: 'postgresql',
config: {
host: 'localhost',
port: 5432,
database: 'myapp',
user: 'postgres',
password: 'password',
},
name: 'MainDatabase' // Recommended: used for IoC registration
})
export class MyDatabase extends AsenaDatabaseService<BunSQLDatabase<typeof Schemas>> {}💡 Schema Export Pattern
We export all schemas as a single object for better TypeScript support:
// database/schemas/index.ts
import * as User from './user.schema';
export default { ...User };Why?
- ✅ Full IntelliSense in queries
- ✅ Type-safe column access
- ✅ Autocomplete for table names
- ✅ Compile-time error catching
Usage:
export class MyDatabase extends AsenaDatabaseService<BunSQLDatabase<typeof Schemas>> {
// Now you have full type safety! 🎉
}This is Drizzle's recommended pattern for optimal developer experience.
3. Create Repository
import { Repository, BaseRepository } from '@asenajs/asena-drizzle';
import { eq } from 'drizzle-orm';
@Repository({
table: users,
databaseService: 'MainDatabase',
})
export class UserRepository extends BaseRepository<typeof users,BunSQLDatabase<typeof Schemas>> {
async findByEmail(email: string) {
return this.findOne(eq(users.email, email));
}
async findActiveUsers() {
return this.findAll(eq(users.isActive, true));
}
}Type-Safe Repository Pattern
Always provide both generic parameters to your repository for full TypeScript support:
// First parameter: Your table schema
// Second parameter: Your database connection type
export class UserRepository extends BaseRepository<typeof users, NodePgDatabase<typeof Schemas>> {
// Now you have full IntelliSense and type checking!
}Why this matters: Without the database type parameter, TypeScript cannot infer the correct query builder methods, and you'll lose IDE autocomplete features.
Available Database Types:
| Database | Driver | Type to Use |
|---|---|---|
| PostgreSQL | pg | NodePgDatabase<typeof Schemas> |
| MySQL | mysql2 | MySql2Database<typeof Schemas> |
| BunSQL (PostgreSQL) | Bun native (type: 'bun-sql') | BunSQLDatabase<typeof Schemas> |
Complete Example:
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import { users } from './schema';
@Repository({ table: users, databaseService: 'MainDatabase' })
export class UserRepository extends BaseRepository<typeof users, NodePgDatabase<typeof Schemas>> {
// Type-safe methods with IntelliSense
async findByEmail(email: string) {
// findOne() is inherited; `this.db` is the raw drizzle connection for anything else
return this.findOne(eq(users.email, email));
}
}4. Activate the Transaction Post-Processor (optional)
If you plan to use @Transaction (see Transactions below), drop a @Drizzle-decorated class somewhere in your source folder — src/config/ is the convention. AsenaJS only scans your source files, never node_modules, so the transaction post-processor must be subclassed inside your project to be discovered.
// src/config/AppDrizzle.ts
import { Drizzle, TransactionPostProcessor } from '@asenajs/asena-drizzle';
@Drizzle({ defaultDb: 'MainDatabase' })
export class AppDrizzle extends TransactionPostProcessor {}The body is intentionally empty — this class exists only so AsenaJS's component scanner picks it up. Setting defaultDb lets every @Transaction() call site omit the database option in single-database projects.
Skip this step if you don't need transactions
The package works fine without @Drizzle — you'll still get repositories, the typed query builder, pagination, and BaseRepository#transaction(cb). You only need this step to enable the @Transaction decorator.
5. Use in Services
import { Service } from '@asenajs/asena/decorators';
import { Inject } from '@asenajs/asena/decorators/ioc';
@Service('UserService')
export class UserService {
@Inject('UserRepository')
private userRepository: UserRepository;
async createUser(name: string, email: string) {
return this.userRepository.create({ name, email });
}
async getAllUsers() {
return this.userRepository.findAll();
}
async getUsersPaginated(page = 1, limit = 10) {
return this.userRepository.paginate(page, limit);
}
}@Database Decorator API
The @Database decorator configures a database connection:
@Database({
type: 'postgresql' | 'mysql' | 'bun-sql',
config: {
// All five are optional: supply them, or a connectionString, not both
host?: string;
port?: number;
database?: string;
user?: string;
password?: string;
ssl?: boolean;
connectionString?: string; // Replaces the five fields above - see "Connection String" below
name?: string; // Optional: shown in the connection log
pool?: { // Optional: driver-agnostic pool sizing, all durations in ms
max?: number;
idleTimeoutMs?: number;
connectTimeoutMs?: number;
maxLifetimeMs?: number;
};
extra?: Record<string, unknown>; // Optional: driver-native options, spread last
},
name?: string; // Optional: service name (recommended for multiple databases)
logger?: ServerLogger; // Optional: where the adapter logs connection events
drizzleConfig?: {
logger?: boolean; // Enable SQL query logging
schema?: any; // Your Drizzle schema
configPath?: string; // Path to a drizzle config file
}
})The pool shape is exported as DatabasePoolConfig if you want to build it separately.
The pool is released on shutdown
AsenaDatabaseService connects from an @OnStart and releases the pool from an @OnStop, so server.stop() hands the connections back.
Nothing did that before the framework grew a stop phase: the pool outlived the server that opened it. That is invisible for a process that exits straight after, and fatal for a test run where every file boots its own container — the pools accumulate until Postgres answers sorry, too many clients already, in whichever test happened to run last.
@Repository Decorator API
The @Repository decorator configures a repository:
@Repository({
table: DrizzleTable, // Your Drizzle table schema (must have 'id' column)
databaseService: string, // Name of the @Database service
name?: string // Optional: service name (defaults to class name)
})@Drizzle Decorator API
@Drizzle activates the transaction post-processor (see Step 4 of Quick Start). Apply it to a class extending TransactionPostProcessor placed in your source folder:
@Drizzle({
defaultDb?: string; // Optional: name of the @Database service used when
// @Transaction() omits the `database` option
})The decorator chains @PostProcessor() onto your subclass and writes the options as metadata, which TransactionPostProcessor reads at bootstrap. You can also apply it with no arguments (@Drizzle()) when you'd rather always specify database explicitly on each @Transaction.
Database Configuration
PostgreSQL
@Database({
type: 'postgresql',
config: {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432'),
database: process.env.DB_NAME || 'myapp',
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || 'password',
// Optional: driver-agnostic connection pool settings
pool: {
max: 20, // pg default: 20
idleTimeoutMs: 30_000, // pg default: 30000
connectTimeoutMs: 2_000, // pg default: 2000
},
},
name: 'MainDatabase'
})
export class PostgresDB extends AsenaDatabaseService {}Connection Pool
pool is driver-agnostic: every duration on it is milliseconds, and each adapter translates it into its own driver's vocabulary. A field left undefined keeps the adapter's default, so adding a pool block never changes anything you did not ask to change.
| Field | postgresql (pg) | mysql (mysql2) | bun-sql (Bun.SQL) |
|---|---|---|---|
max | max — default 20 | connectionLimit — default 10 | max — Bun's own default (10) |
idleTimeoutMs | idleTimeoutMillis — default 30000 | idleTimeout — unset by default | idleTimeout, converted to seconds |
connectTimeoutMs | connectionTimeoutMillis — default 2000 | connectTimeout — unset by default | connectionTimeout, converted to seconds |
maxLifetimeMs | maxLifetimeSeconds, converted to seconds; unset by default | ❌ no mysql2 equivalent — ignored | maxLifetime, converted to seconds |
extra is the escape hatch
config.extra is a Record<string, unknown> spread last into the driver's option object, so it also wins over everything the rest of the config produced. It is passed through unvalidated and is not portable between database types.
config: {
host: 'localhost', port: 5432, database: 'myapp', user: 'postgres', password: '…',
pool: { max: 50 },
extra: { application_name: 'billing-api' }, // pg-specific
}New
These numbers used to be literals inside each adapter, unreachable from configuration — the same image could ship an API wanting 50 connections and a worker wanting 5, and neither could say so. The former literals became the defaults, so an application that configures no pool block behaves exactly as before.
MySQL
@Database({
type: 'mysql',
config: {
host: 'localhost',
port: 3306,
database: 'myapp',
user: 'root',
password: 'password',
},
name: 'MySQLDB'
})
export class MySQLDatabase extends AsenaDatabaseService {}BunSQL
@Database({
type: 'bun-sql',
config: {
host: 'localhost',
port: 5432,
database: 'myapp',
user: 'postgres',
password: 'password',
},
name: 'BunSQLDB'
})
export class BunSQLDatabase extends AsenaDatabaseService {}Connection String
Every adapter honours connectionString. Set it and leave the discrete fields out — they are optional, and when a connection string is present they are not sent to the driver at all.
@Database({
type: 'postgresql',
config: {
connectionString: process.env.DATABASE_URL,
// Still applied on top of the URL
pool: { max: 20 },
},
name: 'MainDatabase'
})
export class DatabaseFromURL extends AsenaDatabaseService {}Each driver takes it under its own name — pg as connectionString, mysql2 as uri, Bun.SQL as url — but the rule above is the same for all three. ssl, pool and extra still apply.
Supplying both is not a merge
Setting a connection string and the discrete fields does not fill the URL's gaps from the fields. The drivers do not even agree on who wins: pg lets the URL win, while mysql2 keeps any truthy discrete option and ignores the URI entirely. And on pg, a key the URL omits falls back to PGHOST/PGPORT/pg's defaults — never to the discrete value you supplied, so { port: 5555, connectionString: 'postgres://u:p@host/db' } resolves to 5432.
That is why Asena omits the discrete fields outright rather than emitting both. Pick one.
Where ssl fits
For pg, an ssl/sslmode in the query string wins in both directions — it overrides config.ssl whichever way each is set — because pg re-parses the URL over the whole config. config.ssl only decides what the URL is silent about.
For mysql2 and bun-sql the priority is the other way round: an explicit ssl: true beats the URL, and the URL applies when ssl is unset or false.
Fixed in 0.10.0
connectionString was accepted by the config type and documented as supported, but only bun-sql ever read it — and even there it was dropped along with ssl and the pool size. The postgresql and mysql adapters built their options from the discrete fields alone, so a URL-configured application silently connected somewhere else: pg fell through to PGHOST and the OS username, mysql2 to localhost:3306.
The five discrete fields are now optional, so the host: '', port: 0 ceremony this page used to show is gone. DatabaseAdapter.createConnectionString() was removed in the same change.
Repository Methods
The BaseRepository provides a comprehensive set of CRUD methods:
Finding Records
findById(id)
Find a record by its ID.
const user = await userRepository.findById('123e4567-e89b-12d3-a456-426614174000');findOne(where)
Find a single record matching the condition.
import { eq } from 'drizzle-orm';
const user = await userRepository.findOne(eq(users.email, 'john@example.com'));findAll(where?)
Find all records, optionally filtered.
// All records
const allUsers = await userRepository.findAll();
// Filtered
const activeUsers = await userRepository.findAll(eq(users.isActive, true));Creating Records
create(data)
Create a single record.
const newUser = await userRepository.create({
name: 'John Doe',
email: 'john@example.com'
});createMany(data[])
Create multiple records at once.
const newUsers = await userRepository.createMany([
{ name: 'John', email: 'john@example.com' },
{ name: 'Jane', email: 'jane@example.com' }
]);Updating Records
updateById(id, data)
Update a record by ID.
const updated = await userRepository.updateById(
'123e4567-e89b-12d3-a456-426614174000',
{ name: 'John Updated' }
);update(where, data)
Update records matching a condition.
import { eq } from 'drizzle-orm';
await userRepository.update(
eq(users.isActive, false),
{ isActive: true }
);Deleting Records
deleteById(id)
Delete a record by ID.
await userRepository.deleteById('123e4567-e89b-12d3-a456-426614174000');delete(where)
Delete records matching a condition.
import { eq } from 'drizzle-orm';
await userRepository.delete(eq(users.isActive, false));Counting Records
count()
Count all records.
const total = await userRepository.count();countBy(where)
Count records matching a condition.
import { eq } from 'drizzle-orm';
const activeCount = await userRepository.countBy(eq(users.isActive, true));Pagination
paginate(page, limit, where?, orderBy?)
Get paginated results.
import { desc } from 'drizzle-orm';
const result = await userRepository.paginate(1, 10, undefined, desc(users.createdAt));
// result.data - array of records
// result.total - total count
// result.page - current page
// result.limit - items per page
// result.totalPages - total pagesDeterministic by default
When you do not provide orderBy, the repository falls back to asc(table.id) so rows do not interleave across pages. Pass an explicit orderBy whenever you need a different sort.
Existence Check
exists(where)
Check if a record exists.
import { eq } from 'drizzle-orm';
const exists = await userRepository.exists(eq(users.email, 'john@example.com'));Field Shortcuts
These two helpers wrap the most common eq(column, value) lookups so you don't have to import eq from drizzle-orm for one-off checks. Both are fully type-safe — field must be a key of the row type, and value is constrained to that column's inferred type.
findBy(field, value)
Equivalent to findAll(eq(table[field], value)).
const activeUsers = await userRepository.findBy('status', 'active');existsBy(field, value)
Equivalent to exists(eq(table[field], value)).
const taken = await userRepository.existsBy('email', 'john@example.com');Bulk Aliases
updateMany / deleteMany are semantic aliases of update / delete that respect the documented public API surface. They behave identically to their counterparts.
updateMany(where, data)
import { eq } from 'drizzle-orm';
await userRepository.updateMany(eq(users.isActive, false), { isActive: true });deleteMany(where)
import { eq } from 'drizzle-orm';
const removedCount = await userRepository.deleteMany(eq(users.isActive, false));Programmatic Transaction
transaction(callback, options?)
Run callback inside a Drizzle transaction. When called from within an active @Transaction scope (see Transactions below), it opens a SAVEPOINT; otherwise it starts a brand-new top-level transaction.
await userRepository.transaction(async () => {
await userRepository.create({ name: 'Ada', email: 'ada@example.com' });
await profileRepository.create({ userId: 'ada-id', bio: '…' });
}, { isolationLevel: 'serializable' });The optional second argument forwards isolationLevel and accessMode to Drizzle's db.transaction(cb, config).
Advanced Queries
Complex Filtering
import { eq, and, or, gt, lt, like } from 'drizzle-orm';
@Repository({ table: users, databaseService: 'MainDatabase' })
export class UserRepository extends BaseRepository<typeof users> {
async findAdultActiveUsers() {
return this.findAll(
and(
eq(users.isActive, true),
gt(users.age, 18)
)
);
}
async searchUsers(query: string) {
return this.findAll(
or(
like(users.name, `%${query}%`),
like(users.email, `%${query}%`)
)
);
}
}Joins and Relations
import { pgTable, uuid, text, foreignKey } from 'drizzle-orm/pg-core';
export const posts = pgTable('posts', {
id: uuid('id').primaryKey().defaultRandom(),
title: text('title').notNull(),
authorId: uuid('author_id').notNull().references(() => users.id),
});
@Repository({ table: posts, databaseService: 'MainDatabase' })
export class PostRepository extends BaseRepository<typeof posts> {
async findPostsWithAuthors() {
const db = this.db;
return db
.select({
post: posts,
author: users
})
.from(posts)
.leftJoin(users, eq(posts.authorId, users.id));
}
}Raw Database Access
@Repository({ table: users, databaseService: 'MainDatabase' })
export class UserRepository extends BaseRepository<typeof users> {
async complexQuery() {
const db = this.db;
// Use Drizzle's full API
return db
.select()
.from(users)
.where(eq(users.isActive, true))
.orderBy(desc(users.createdAt))
.limit(10);
}
async rawSQL() {
const db = this.db;
// Execute raw SQL if needed
return db.execute(sql`
SELECT * FROM users
WHERE created_at > NOW() - INTERVAL '7 days'
`);
}
}Multiple Databases
You can connect to multiple databases simultaneously:
// Primary database
@Database({
type: 'postgresql',
config: { /* primary db config */ },
name: 'PrimaryDB'
})
export class PrimaryDatabase extends AsenaDatabaseService {}
// Analytics database
@Database({
type: 'mysql',
config: { /* analytics db config */ },
name: 'AnalyticsDB'
})
export class AnalyticsDatabase extends AsenaDatabaseService {}
// Use different databases
@Repository({ table: users, databaseService: 'PrimaryDB' })
export class UserRepository extends BaseRepository<typeof users> {}
@Repository({ table: events, databaseService: 'AnalyticsDB' })
export class EventRepository extends BaseRepository<typeof events> {}Transactions
asena-drizzle ships a Spring-style @Transaction decorator backed by Bun's native AsyncLocalStorage. Repository calls made inside a @Transaction-wrapped method automatically pick up the active transaction — you do not have to thread a tx parameter through your code.
Setup required
@Transaction only works once you have activated the post-processor with a @Drizzle-decorated class in your source folder — see Step 4 of Quick Start. Without it the decorator silently does nothing because AsenaJS never sees the post-processor.
@Transaction Decorator
import { Service } from '@asenajs/asena/decorators';
import { Inject } from '@asenajs/asena/decorators/ioc';
import { Transaction } from '@asenajs/asena-drizzle';
@Service('AccountService')
export class AccountService {
@Inject('UserRepository')
private userRepo: UserRepository;
@Inject('AuditRepository')
private auditRepo: AuditRepository;
// Uses defaultDb from @Drizzle({ defaultDb: 'MainDatabase' })
@Transaction()
async register(payload: { email: string; name: string }) {
const user = await this.userRepo.create(payload);
await this.auditRepo.create({ userId: user.id, event: 'register' });
// Either both rows commit, or both roll back atomically.
return user;
}
}When you have multiple @Database services, pass the target name explicitly — it overrides any defaultDb:
@Transaction({ database: 'AnalyticsDB' })
async trackEvent(...) { ... }If neither @Drizzle.defaultDb nor @Transaction.database is set, the post-processor fails fast at registration time with a descriptive error.
Propagation Modes
| Mode | If a transaction is active | If no transaction is active |
|---|---|---|
REQUIRED (default) | Joins the existing transaction (no new one is started). | Starts a new top-level transaction. |
NESTED | Opens a SAVEPOINT inside the existing transaction. Inner failures roll back to the savepoint without aborting the outer transaction. | Starts a new top-level transaction. |
REQUIRES_NEW | Suspends the outer transaction and runs in an independent top-level transaction (a fresh connection is taken from the pool). | Starts a new top-level transaction. |
@Transaction({
database: 'MainDatabase',
propagation: 'NESTED',
isolationLevel: 'serializable',
})
async bestEffortAudit(userId: string) {
// Inner failure rolls back to a savepoint; the outer caller can keep going.
}
@Transaction({
database: 'MainDatabase',
propagation: 'REQUIRES_NEW',
})
async writeAuditLog(event: AuditEvent) {
// Survives even if the surrounding transaction rolls back — useful for
// audit/telemetry writes that must persist independently.
}Isolation & Access Mode
isolationLevel and accessMode are forwarded to Drizzle's db.transaction(cb, config) on the top-level paths (REQUIRED starting a new transaction, and REQUIRES_NEW). A NESTED call inside an existing transaction opens a SAVEPOINT with no config, so it inherits the outer transaction's isolation:
@Transaction({
database: 'MainDatabase',
isolationLevel: 'repeatable read',
accessMode: 'read only',
})
async snapshot() {
// …
}Supported values:
isolationLevel:'read uncommitted' | 'read committed' | 'repeatable read' | 'serializable'accessMode:'read only' | 'read write'
Dialects that don't recognize a given value silently ignore it (e.g. SQLite vs PostgreSQL).
Programmatic Boundary
Use BaseRepository#transaction(callback, options?) when you need a transaction inside a single method without lifting the boundary up to a service-level decorator. It is ALS-aware: inside an active @Transaction scope it opens a SAVEPOINT, otherwise it starts a new top-level transaction.
Raw Drizzle Escape Hatch
If you need access to Drizzle's full query builder inside a transaction (joins, raw SQL, dialect-specific helpers), reach for the underlying db.transaction(...):
@Service()
export class OrderService {
@Inject('OrderRepository')
private orderRepo: OrderRepository;
@Inject('InventoryRepository')
private inventoryRepo: InventoryRepository;
async createOrder(items: Array<{ productId: string; quantity: number }>) {
return this.orderRepo.db.transaction(async (tx) => {
const order = await tx.insert(orders).values({
total: 100,
status: 'pending',
}).returning();
for (const item of items) {
await tx.update(inventory)
.set({ stock: sql`stock - ${item.quantity}` })
.where(eq(inventory.productId, item.productId));
}
return order[0];
});
}
}Self-invocation
@Transaction only wraps actual class methods. Arrow-function class properties (run = async () => …) live on the instance, not the prototype, and are not intercepted. Use the standard async method() { … } syntax.
Roadmap — full auto-resolution
Today, single-database projects can use @Drizzle({ defaultDb }) once and call @Transaction() with no arguments thereafter. Multi-database projects still need to name the target explicitly. Once AsenaJS core ships an afterAllComponentsRegistered post-processor hook (see docs/asena-core-feature-request-afterAllComponentsRegistered.md in the asena-drizzle repository), v1.3.0 will skip even the defaultDb step when exactly one @Database service is registered, mirroring Spring Boot's auto-wired repositories.
Best Practices
1. Use Repository Pattern
// ✅ Good: Business logic in service, data access in repository
@Repository({ table: users, databaseService: 'MainDB' })
export class UserRepository extends BaseRepository<typeof users> {
async findByEmail(email: string) {
return this.findOne(eq(users.email, email));
}
}
@Service()
export class UserService {
@Inject(UserRepository)
private userRepo: UserRepository;
async createUser(data: any) {
const existing = await this.userRepo.findByEmail(data.email);
if (existing) throw new Error('Email exists');
return this.userRepo.create(data);
}
}
// ❌ Bad: Direct database access in service
@Service()
export class UserService {
async createUser(data: any) {
const db = this.db;
const existing = await db.select().from(users)...
}
}2. Type Safety
// ✅ Good: Full type inference
const user = await userRepository.create({
name: 'John',
email: 'john@example.com'
});
// user.id, user.name, user.email are all typed!
// ❌ Bad: Losing type safety
const user: any = await userRepository.create({...});3. Use Pagination for Large Datasets
// ✅ Good: Paginated results
const page1 = await userRepository.paginate(1, 20);
const page2 = await userRepository.paginate(2, 20);
// ❌ Bad: Loading all records
const allUsers = await userRepository.findAll(); // Could be millions!4. Custom Repository Methods
// ✅ Good: Encapsulate complex queries
@Repository({ table: users, databaseService: 'MainDB' })
export class UserRepository extends BaseRepository<typeof users> {
async findRecentActiveUsers(days: number = 7) {
const db = this.db;
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - days);
return db
.select()
.from(users)
.where(
and(
eq(users.isActive, true),
gt(users.lastLoginAt, cutoffDate)
)
);
}
}Related Documentation
- Services
- Dependency Injection
- Component Lifecycle - when the pool is opened and released
- Inheritance - Sharing repository methods through a base class
- Configuration
- Drizzle ORM Documentation
Next Steps: