Skip to main content

@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

MetricValue
Bundle Size~4 KB (minified)
Dependencies@kysera/core (workspace)
Peer Dependencieskysely >=0.28.14, @kysera/repository

Exports

// Main plugin
export { timestampsPlugin } from './index'

// Types
export type { TimestampsOptions, TimestampMethods, TimestampsRepository }

// Schema (optional, requires Zod)
export { TimestampsOptionsSchema, type TimestampsOptionsSchemaType } from './schema'

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
* Used by touch(), updateMany(), and touchMany() methods
* @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 (affects touch(), updateMany(), touchMany(), createMany())
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<T[]>
touchMany(ids: (number | string)[]): Promise<void>

// Utilities
touch(id: number): Promise<void>
createWithoutTimestamps(input: unknown): Promise<T>
updateWithoutTimestamp(id: number, 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. Uses the configured primaryKeyColumn for the WHERE clause.

async updateMany(ids: (number | string)[], input: unknown): Promise<T[]>

Returns: Array of updated records.

Example:

const updated = await postRepo.updateMany([1, 2, 3], { status: 'published' })
console.log(`Updated ${updated.length} posts`)

touchMany

Update only timestamps for multiple records. Uses the configured primaryKeyColumn for the WHERE clause.

async touchMany(ids: (number | string)[]): Promise<void>

Example:

await postRepo.touchMany([1, 2, 3, 4, 5])

// For UUID primary keys, configure primaryKeyColumn:
// timestampsPlugin({ primaryKeyColumn: 'uuid' })
await userRepo.touchMany(['uuid-1', 'uuid-2', 'uuid-3'])

Utilities

touch

Update only the updated_at timestamp for a record. Uses the configured primaryKeyColumn for the WHERE clause.

async touch(id: number): Promise<void>

Parameters:

  • id - Primary key of the record (numeric)

Returns: void (updates the record in place)

Example:

// Update user's last activity timestamp
await userRepo.touch(userId)

// Fetch the user to see the updated timestamp
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:

OperationOverhead
create+0.1ms
update+0.1ms
findRecentlyCreated+0.2ms
createMany<1ms regardless of count

Primary Key Column Support

The primaryKeyColumn option is used by all ID-based methods:

MethodRespects primaryKeyColumn?
create()N/A
update()N/A
touch(id)✅ Yes
updateMany(ids)✅ Yes
touchMany(ids)✅ Yes
createMany()✅ Yes (for MySQL fallback)

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']
})

Schema Validation (Optional)

The timestamps plugin provides optional Zod schemas for configuration validation. This is useful for CLI tools, configuration file parsing, and runtime validation.

:::info Separate Export Schemas are exported from @kysera/timestamps/schema to keep Zod as an optional dependency. The main @kysera/timestamps export works without Zod installed. :::

TimestampsOptionsSchema

Zod schema for validating TimestampsOptions configuration.

import { TimestampsOptionsSchema } from '@kysera/timestamps/schema'

// Validate configuration
const result = TimestampsOptionsSchema.safeParse({
createdAtColumn: 'created_at',
updatedAtColumn: 'updated_at',
setUpdatedAtOnInsert: true,
dateFormat: 'iso'
})

if (result.success) {
console.log('Valid config:', result.data)
} else {
console.error('Invalid config:', result.error.issues)
}

Schema Fields

const TimestampsOptionsSchema = z.object({
createdAtColumn: z.string().optional(),
updatedAtColumn: z.string().optional(),
setUpdatedAtOnInsert: z.boolean().optional(),
tables: z.array(z.string()).optional(),
excludeTables: z.array(z.string()).optional(),
getTimestamp: z.function().optional(),
dateFormat: z.enum(['iso', 'unix', 'date']).optional(),
primaryKeyColumn: z.string().optional()
})

Type Inference

import { TimestampsOptionsSchema, type TimestampsOptionsSchemaType } from '@kysera/timestamps/schema'

// Type inferred from schema
type Options = TimestampsOptionsSchemaType

// Same as TimestampsOptions interface
const config: Options = {
createdAtColumn: 'created',
updatedAtColumn: 'modified',
dateFormat: 'iso'
}

See Also