@kysera/timestamps
Automatic timestamp management plugin for Kysera - Automatically manage created_at and updated_at timestamps on your entities.
Installation
npm install @kysera/timestamps
Overview
| Metric | Value |
|---|---|
| Version | 0.7.0 |
| Bundle Size | ~4 KB (minified) |
| Dependencies | @kysera/core (workspace) |
| Peer Dependencies | kysely >=0.28.8, @kysera/repository |
Exports
// Main plugin
export { timestampsPlugin } from './index'
// Types
export type { TimestampsOptions, TimestampsMethods, TimestampsRepository }
timestampsPlugin
Creates a timestamps plugin instance.
function timestampsPlugin(options?: TimestampsOptions): Plugin
TimestampsOptions
interface TimestampsOptions {
/**
* Name of the created_at column
* @default 'created_at'
*/
createdAtColumn?: string
/**
* Name of the updated_at column
* @default 'updated_at'
*/
updatedAtColumn?: string
/**
* Whether to set updated_at on insert operations
* @default false
*/
setUpdatedAtOnInsert?: boolean
/**
* List of tables to apply timestamps to (whitelist)
* If not specified, all tables will have timestamps
*/
tables?: string[]
/**
* List of tables to exclude from timestamps (blacklist)
*/
excludeTables?: string[]
/**
* Custom timestamp generator function
* @default () => new Date()
*/
getTimestamp?: () => Date | string | number
/**
* Date format for timestamps
* - 'iso': ISO 8601 string (default)
* - 'unix': Unix timestamp in milliseconds
* - 'date': JavaScript Date object
* @default 'iso'
*/
dateFormat?: 'iso' | 'unix' | 'date'
/**
* Name of the primary key column
* NOTE: Only affects touch() method. updateMany/touchMany use hardcoded 'id'
* @default 'id'
*/
primaryKeyColumn?: string
/**
* Logger for plugin operations
*/
logger?: KyseraLogger
}
Configuration Examples
import { timestampsPlugin } from '@kysera/timestamps'
// Default configuration (zero config)
const plugin = timestampsPlugin()
// Custom column names
const plugin = timestampsPlugin({
createdAtColumn: 'created',
updatedAtColumn: 'modified'
})
// Unix timestamps
const plugin = timestampsPlugin({
dateFormat: 'unix',
getTimestamp: () => Date.now()
})
// Only specific tables
const plugin = timestampsPlugin({
tables: ['users', 'posts', 'comments']
})
// Exclude specific tables
const plugin = timestampsPlugin({
excludeTables: ['audit_logs', 'migrations']
})
// Custom timestamp source
const plugin = timestampsPlugin({
getTimestamp: () => new Date().toISOString()
})
// Set updated_at on insert
const plugin = timestampsPlugin({
setUpdatedAtOnInsert: true
})
// Custom primary key (only affects touch())
const plugin = timestampsPlugin({
primaryKeyColumn: 'user_id'
})
Repository Methods
When a repository is extended by the timestamps plugin, the following methods are added:
TimestampsMethods Interface
interface TimestampsMethods<T> {
// Date range queries
findCreatedAfter(date: Date | string): Promise<T[]>
findCreatedBefore(date: Date | string): Promise<T[]>
findCreatedBetween(start: Date | string, end: Date | string): Promise<T[]>
findUpdatedAfter(date: Date | string): Promise<T[]>
// Recent records
findRecentlyCreated(limit?: number): Promise<T[]>
findRecentlyUpdated(limit?: number): Promise<T[]>
// Batch operations
createMany(inputs: unknown[]): Promise<T[]>
updateMany(ids: (number | string)[], input: unknown): Promise<void>
touchMany(ids: (number | string)[]): Promise<void>
// Utilities
touch(id: number | string): Promise<T>
createWithoutTimestamps(input: unknown): Promise<T>
updateWithoutTimestamp(id: number | string, input: unknown): Promise<T>
getTimestampColumns(): { createdAt: string; updatedAt: string }
}
Date Range Queries
findCreatedAfter
Find records created after a specific date.
async findCreatedAfter(date: Date | string): Promise<T[]>
Parameters:
date- Date object or ISO string
Example:
const weekAgo = new Date()
weekAgo.setDate(weekAgo.getDate() - 7)
const recentPosts = await postRepo.findCreatedAfter(weekAgo)
findCreatedBefore
Find records created before a specific date.
async findCreatedBefore(date: Date | string): Promise<T[]>
Example:
const oldPosts = await postRepo.findCreatedBefore('2024-01-01')
findCreatedBetween
Find records created within a date range.
async findCreatedBetween(start: Date | string, end: Date | string): Promise<T[]>
Example:
const posts = await postRepo.findCreatedBetween(
'2024-01-01',
'2024-01-31'
)
findUpdatedAfter
Find records updated after a specific date.
async findUpdatedAfter(date: Date | string): Promise<T[]>
Example:
const yesterday = new Date()
yesterday.setDate(yesterday.getDate() - 1)
const updatedPosts = await postRepo.findUpdatedAfter(yesterday)
Recent Records
findRecentlyCreated
Get the most recently created records.
async findRecentlyCreated(limit?: number): Promise<T[]>
Parameters:
limit- Maximum number of records (default: 10)
Example:
// Get 10 most recently created posts
const latestPosts = await postRepo.findRecentlyCreated()
// Get 50 most recently created
const latestPosts = await postRepo.findRecentlyCreated(50)
findRecentlyUpdated
Get the most recently updated records.
async findRecentlyUpdated(limit?: number): Promise<T[]>
Example:
const recentlyUpdated = await postRepo.findRecentlyUpdated(25)
Batch Operations
createMany
Create multiple records with automatic timestamps.
async createMany(inputs: unknown[]): Promise<T[]>
Example:
const posts = await postRepo.createMany([
{ title: 'Post 1', content: '...' },
{ title: 'Post 2', content: '...' },
{ title: 'Post 3', content: '...' }
])
// All posts have created_at set automatically
updateMany
Update multiple records with automatic updated_at.
async updateMany(ids: (number | string)[], input: unknown): Promise<void>
updateMany() uses hardcoded 'id' for the WHERE clause. For tables with custom primary keys, use individual update() calls.
Example:
// Requires primary key column named 'id'
await postRepo.updateMany([1, 2, 3], { status: 'published' })
touchMany
Update only timestamps for multiple records.
async touchMany(ids: (number | string)[]): Promise<void>
touchMany() uses hardcoded 'id' for the WHERE clause. For tables with custom primary keys, use touch() in a loop.
Example:
// Requires primary key column named 'id'
await postRepo.touchMany([1, 2, 3, 4, 5])
// For custom primary keys, use touch() in a loop:
for (const userId of userIds) {
await userRepo.touch(userId)
}
Utilities
touch
Update only the updated_at timestamp for a record.
async touch(id: number | string): Promise<T>
Parameters:
id- Primary key of the record
Returns: The updated record
Example:
// Update user's last activity
await userRepo.touch(userId)
const user = await userRepo.findById(userId)
console.log(`User last active: ${user.updated_at}`)
createWithoutTimestamps
Create a record bypassing automatic timestamp setting.
async createWithoutTimestamps(input: unknown): Promise<T>
Example:
// Useful for data imports
const importedPost = await postRepo.createWithoutTimestamps({
title: 'Imported Post',
content: '...',
created_at: originalCreatedAt // Preserve original date
})
updateWithoutTimestamp
Update a record without changing updated_at.
async updateWithoutTimestamp(id: number | string, input: unknown): Promise<T>
Example:
// Update view count without changing updated_at
await postRepo.updateWithoutTimestamp(postId, {
view_count: post.view_count + 1
})
getTimestampColumns
Get the configured column names.
getTimestampColumns(): { createdAt: string; updatedAt: string }
Example:
const columns = postRepo.getTimestampColumns()
console.log(columns) // { createdAt: 'created_at', updatedAt: 'updated_at' }
Automatic Timestamp Setting
On Create
The plugin automatically sets created_at when inserting records:
const post = await postRepo.create({
title: 'Hello World',
content: 'My first post'
})
console.log(post.created_at) // 2024-01-15T10:30:00.000Z
On Update
The plugin automatically sets updated_at when updating records:
await postRepo.update(postId, { title: 'Updated Title' })
// updated_at is set automatically
Query Interception
The plugin intercepts insert and update operations:
// Plugin implementation (simplified)
interceptQuery(qb, context) {
const timestamp = getTimestamp()
if (context.operation === 'insert') {
return qb.set({ [createdAtColumn]: timestamp })
}
if (context.operation === 'update') {
return qb.set({ [updatedAtColumn]: timestamp })
}
return qb
}
Usage with Plugin Container
import { createORM, createRepositoryFactory } from '@kysera/repository'
import { timestampsPlugin } from '@kysera/timestamps'
import { z } from 'zod'
// createORM creates a plugin container (repository manager), not a traditional ORM
const orm = await createORM(db, [
timestampsPlugin() // Zero config!
])
const postRepo = orm.createRepository((executor) => {
const factory = createRepositoryFactory(executor)
return factory.create({
tableName: 'posts',
mapRow: (row) => ({
id: row.id,
title: row.title,
content: row.content,
createdAt: row.created_at,
updatedAt: row.updated_at
}),
schemas: {
create: z.object({
title: z.string().min(1),
content: z.string()
})
}
})
})
// created_at is set automatically
const post = await postRepo.create({
title: 'Hello World',
content: 'My first post'
})
// updated_at is set automatically on update
await postRepo.update(post.id, { title: 'Updated Title' })
Database Schema
-- PostgreSQL
ALTER TABLE posts ADD COLUMN created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
ALTER TABLE posts ADD COLUMN updated_at TIMESTAMP;
CREATE INDEX idx_posts_created_at ON posts(created_at DESC);
CREATE INDEX idx_posts_updated_at ON posts(updated_at DESC);
-- MySQL
ALTER TABLE posts ADD COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP;
ALTER TABLE posts ADD COLUMN updated_at DATETIME;
-- SQLite
ALTER TABLE posts ADD COLUMN created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP;
ALTER TABLE posts ADD COLUMN updated_at TEXT;
TypeScript Types
TimestampsRepository
type TimestampsRepository<Entity, DB> = Repository<Entity, DB> & TimestampsMethods<Entity>
Database Schema Type
interface PostsTable {
id: Generated<number>
title: string
content: string
created_at: Generated<Date> // Generated - has default
updated_at: Date | null // Nullable for new records
}
Performance
The timestamps plugin adds minimal overhead:
| Operation | Overhead |
|---|---|
| create | +0.1ms |
| update | +0.1ms |
| findRecentlyCreated | +0.2ms |
| createMany | <1ms regardless of count |
Known Limitations
Primary Key Column
The primaryKeyColumn option only affects the touch() method. The following methods use hardcoded 'id':
| Method | Respects primaryKeyColumn? |
|---|---|
create() | N/A |
update() | N/A |
touch(id) | ✅ Yes |
updateMany(ids) | ❌ No - uses 'id' |
touchMany(ids) | ❌ No - uses 'id' |
createMany() | N/A |
Workaround:
// For tables with custom primary keys
for (const userId of userIds) {
await userRepo.touch(userId) // Respects primaryKeyColumn
}
Best Practices
1. Index Timestamp Columns
CREATE INDEX idx_posts_created_at ON posts(created_at DESC);
CREATE INDEX idx_posts_updated_at ON posts(updated_at DESC);
2. Use for Activity Tracking
// Track user activity without explicit field
app.use(async (req, res, next) => {
if (req.user) {
await userRepo.touch(req.user.id)
}
next()
})
3. Combine with Other Plugins
const orm = await createORM(db, [
timestampsPlugin(), // Handles timestamps
softDeletePlugin(), // Handles deleted_at separately
auditPlugin() // Full audit trail
])
4. Exclude System Tables
timestampsPlugin({
excludeTables: ['migrations', 'audit_logs', 'system_config']
})