@kysera/timestamps 0.7.2 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +131 -94
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @kysera/timestamps
|
|
2
2
|
|
|
3
|
-
> Automatic timestamp management plugin for Kysera - Zero-configuration `created_at` and `updated_at` tracking with powerful query helpers.
|
|
3
|
+
> Automatic timestamp management plugin for Kysera (v0.7.3) - Zero-configuration `created_at` and `updated_at` tracking through @kysera/executor's Unified Execution Layer with powerful query helpers.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@kysera/timestamps)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
## 🎯 Features
|
|
10
10
|
|
|
11
11
|
- ✅ **Zero Configuration** - Works out of the box with sensible defaults
|
|
12
|
-
- ✅ **Automatic Timestamps** - `created_at` on insert, `updated_at` on update
|
|
12
|
+
- ✅ **Automatic Timestamps** - `created_at` on insert, `updated_at` on update via @kysera/executor
|
|
13
13
|
- ✅ **Batch Operations** - Efficient `createMany`, `updateMany`, `touchMany` methods
|
|
14
14
|
- ✅ **Custom Column Names** - Use any column names you want
|
|
15
15
|
- ✅ **Table Filtering** - Whitelist or blacklist specific tables
|
|
16
16
|
- ✅ **Date Formats** - ISO strings, Unix timestamps, or Date objects
|
|
17
17
|
- ✅ **Query Helpers** - 13+ methods for timestamp-based queries
|
|
18
18
|
- ✅ **Type-Safe** - Full TypeScript support with inference
|
|
19
|
-
- ✅ **Plugin Architecture** - Integrates seamlessly
|
|
19
|
+
- ✅ **Plugin Architecture** - Integrates seamlessly via @kysera/executor's Unified Execution Layer
|
|
20
20
|
- ✅ **Production Ready** - Battle-tested with comprehensive test coverage
|
|
21
21
|
|
|
22
22
|
## 📥 Installation
|
|
@@ -59,6 +59,7 @@ CREATE TABLE users (
|
|
|
59
59
|
```typescript
|
|
60
60
|
import { Kysely, PostgresDialect } from 'kysely'
|
|
61
61
|
import { Pool } from 'pg'
|
|
62
|
+
import { createExecutor } from '@kysera/executor'
|
|
62
63
|
import { createORM, createRepositoryFactory } from '@kysera/repository'
|
|
63
64
|
import { timestampsPlugin } from '@kysera/timestamps'
|
|
64
65
|
import { z } from 'zod'
|
|
@@ -77,21 +78,26 @@ interface Database {
|
|
|
77
78
|
// Create database connection
|
|
78
79
|
const db = new Kysely<Database>({
|
|
79
80
|
dialect: new PostgresDialect({
|
|
80
|
-
pool: new Pool({
|
|
81
|
+
pool: new Pool({
|
|
82
|
+
/* config */
|
|
83
|
+
})
|
|
81
84
|
})
|
|
82
85
|
})
|
|
83
86
|
|
|
84
|
-
//
|
|
85
|
-
const
|
|
86
|
-
timestampsPlugin()
|
|
87
|
+
// Step 1: Register timestamps plugin with Unified Execution Layer
|
|
88
|
+
const executor = await createExecutor(db, [
|
|
89
|
+
timestampsPlugin() // ✨ That's it!
|
|
87
90
|
])
|
|
88
91
|
|
|
89
|
-
// Create
|
|
90
|
-
const
|
|
92
|
+
// Step 2: Create ORM with plugin-enabled executor
|
|
93
|
+
const orm = await createORM(executor, [])
|
|
94
|
+
|
|
95
|
+
// Step 3: Create repository
|
|
96
|
+
const userRepo = orm.createRepository(executor => {
|
|
91
97
|
const factory = createRepositoryFactory(executor)
|
|
92
98
|
return factory.create<'users', User>({
|
|
93
99
|
tableName: 'users',
|
|
94
|
-
mapRow:
|
|
100
|
+
mapRow: row => row as User,
|
|
95
101
|
schemas: {
|
|
96
102
|
create: z.object({
|
|
97
103
|
email: z.string().email(),
|
|
@@ -107,15 +113,15 @@ const user = await userRepo.create({
|
|
|
107
113
|
name: 'Alice'
|
|
108
114
|
})
|
|
109
115
|
|
|
110
|
-
console.log(user.created_at)
|
|
111
|
-
console.log(user.updated_at)
|
|
116
|
+
console.log(user.created_at) // ✅ 2024-01-15T10:30:00.000Z
|
|
117
|
+
console.log(user.updated_at) // null (only set on update)
|
|
112
118
|
|
|
113
119
|
// Update - updated_at set automatically!
|
|
114
120
|
const updated = await userRepo.update(user.id, {
|
|
115
121
|
name: 'Alice Smith'
|
|
116
122
|
})
|
|
117
123
|
|
|
118
|
-
console.log(updated.updated_at)
|
|
124
|
+
console.log(updated.updated_at) // ✅ 2024-01-15T10:35:00.000Z
|
|
119
125
|
```
|
|
120
126
|
|
|
121
127
|
---
|
|
@@ -167,9 +173,9 @@ const plugin = timestampsPlugin({
|
|
|
167
173
|
updatedAtColumn: 'updated_at',
|
|
168
174
|
setUpdatedAtOnInsert: false,
|
|
169
175
|
dateFormat: 'iso',
|
|
170
|
-
tables: undefined,
|
|
176
|
+
tables: undefined, // All tables
|
|
171
177
|
excludeTables: undefined, // No exclusions
|
|
172
|
-
primaryKeyColumn: 'id'
|
|
178
|
+
primaryKeyColumn: 'id' // Default primary key
|
|
173
179
|
})
|
|
174
180
|
```
|
|
175
181
|
|
|
@@ -195,8 +201,8 @@ interface Database {
|
|
|
195
201
|
posts: {
|
|
196
202
|
id: Generated<number>
|
|
197
203
|
title: string
|
|
198
|
-
created: Generated<Date>
|
|
199
|
-
modified: Date | null
|
|
204
|
+
created: Generated<Date> // ✅ Custom name
|
|
205
|
+
modified: Date | null // ✅ Custom name
|
|
200
206
|
}
|
|
201
207
|
}
|
|
202
208
|
```
|
|
@@ -230,11 +236,13 @@ const plugin = timestampsPlugin({
|
|
|
230
236
|
#### When to Use Each
|
|
231
237
|
|
|
232
238
|
**Use Whitelist (`tables`) when:**
|
|
239
|
+
|
|
233
240
|
- You have many tables but only a few need timestamps
|
|
234
241
|
- You want explicit control over timestamp tables
|
|
235
242
|
- You're migrating incrementally
|
|
236
243
|
|
|
237
244
|
**Use Blacklist (`excludeTables`) when:**
|
|
245
|
+
|
|
238
246
|
- Most tables need timestamps
|
|
239
247
|
- Only a few system tables should be excluded
|
|
240
248
|
- You want timestamps by default
|
|
@@ -317,12 +325,12 @@ const user = await userRepo.create({
|
|
|
317
325
|
name: 'Alice'
|
|
318
326
|
})
|
|
319
327
|
|
|
320
|
-
console.log(user.created_at)
|
|
321
|
-
console.log(user.updated_at)
|
|
328
|
+
console.log(user.created_at) // 2024-01-15T10:30:00.000Z
|
|
329
|
+
console.log(user.updated_at) // 2024-01-15T10:30:00.000Z (same!)
|
|
322
330
|
|
|
323
331
|
// On update, updated_at changes
|
|
324
332
|
const updated = await userRepo.update(user.id, { name: 'Alice Smith' })
|
|
325
|
-
console.log(updated.updated_at)
|
|
333
|
+
console.log(updated.updated_at) // 2024-01-15T10:35:00.000Z (different)
|
|
326
334
|
```
|
|
327
335
|
|
|
328
336
|
### Custom Primary Key Column
|
|
@@ -346,7 +354,7 @@ const plugin = timestampsPlugin({
|
|
|
346
354
|
// Database schema
|
|
347
355
|
interface Database {
|
|
348
356
|
users: {
|
|
349
|
-
user_id: Generated<number>
|
|
357
|
+
user_id: Generated<number> // Custom primary key
|
|
350
358
|
email: string
|
|
351
359
|
name: string
|
|
352
360
|
created_at: Generated<Date>
|
|
@@ -364,6 +372,8 @@ await userRepo.touch(userId)
|
|
|
364
372
|
|
|
365
373
|
## 🤖 Automatic Behavior
|
|
366
374
|
|
|
375
|
+
The timestamps plugin works through @kysera/executor's Unified Execution Layer to automatically manage timestamps on all operations.
|
|
376
|
+
|
|
367
377
|
### On Create
|
|
368
378
|
|
|
369
379
|
When you call `repository.create()`, the plugin automatically adds `created_at`:
|
|
@@ -378,8 +388,8 @@ const user = await userRepo.create({
|
|
|
378
388
|
// INSERT INTO users (email, name, created_at)
|
|
379
389
|
// VALUES ('bob@example.com', 'Bob', '2024-01-15T10:30:00.000Z')
|
|
380
390
|
|
|
381
|
-
console.log(user.created_at)
|
|
382
|
-
console.log(user.updated_at)
|
|
391
|
+
console.log(user.created_at) // ✅ 2024-01-15T10:30:00.000Z
|
|
392
|
+
console.log(user.updated_at) // null (default behavior)
|
|
383
393
|
```
|
|
384
394
|
|
|
385
395
|
**Manual Override:**
|
|
@@ -389,10 +399,10 @@ console.log(user.updated_at) // null (default behavior)
|
|
|
389
399
|
const user = await userRepo.create({
|
|
390
400
|
email: 'charlie@example.com',
|
|
391
401
|
name: 'Charlie',
|
|
392
|
-
created_at: '2020-01-01T00:00:00.000Z'
|
|
402
|
+
created_at: '2020-01-01T00:00:00.000Z' // ✅ Uses this instead
|
|
393
403
|
})
|
|
394
404
|
|
|
395
|
-
console.log(user.created_at)
|
|
405
|
+
console.log(user.created_at) // 2020-01-01T00:00:00.000Z
|
|
396
406
|
```
|
|
397
407
|
|
|
398
408
|
### On Update
|
|
@@ -409,7 +419,7 @@ const updated = await userRepo.update(userId, {
|
|
|
409
419
|
// SET name = 'New Name', updated_at = '2024-01-15T10:35:00.000Z'
|
|
410
420
|
// WHERE id = 1
|
|
411
421
|
|
|
412
|
-
console.log(updated.updated_at)
|
|
422
|
+
console.log(updated.updated_at) // ✅ 2024-01-15T10:35:00.000Z
|
|
413
423
|
```
|
|
414
424
|
|
|
415
425
|
**Manual Override:**
|
|
@@ -418,7 +428,7 @@ console.log(updated.updated_at) // ✅ 2024-01-15T10:35:00.000Z
|
|
|
418
428
|
// Provide your own updated_at
|
|
419
429
|
const updated = await userRepo.update(userId, {
|
|
420
430
|
name: 'New Name',
|
|
421
|
-
updated_at: '2024-01-01T00:00:00.000Z'
|
|
431
|
+
updated_at: '2024-01-01T00:00:00.000Z' // ✅ Uses this instead
|
|
422
432
|
})
|
|
423
433
|
```
|
|
424
434
|
|
|
@@ -433,18 +443,19 @@ const user = await userRepo.createWithoutTimestamps({
|
|
|
433
443
|
name: 'System User'
|
|
434
444
|
})
|
|
435
445
|
|
|
436
|
-
console.log(user.created_at)
|
|
437
|
-
console.log(user.updated_at)
|
|
446
|
+
console.log(user.created_at) // null (not set)
|
|
447
|
+
console.log(user.updated_at) // null (not set)
|
|
438
448
|
|
|
439
449
|
// Update without modifying updated_at
|
|
440
450
|
const updated = await userRepo.updateWithoutTimestamp(userId, {
|
|
441
451
|
name: 'Silent Update'
|
|
442
452
|
})
|
|
443
453
|
|
|
444
|
-
console.log(updated.updated_at)
|
|
454
|
+
console.log(updated.updated_at) // null (unchanged)
|
|
445
455
|
```
|
|
446
456
|
|
|
447
457
|
**Use Cases:**
|
|
458
|
+
|
|
448
459
|
- Migrating historical data with specific timestamps
|
|
449
460
|
- System operations that shouldn't update timestamps
|
|
450
461
|
- Preserving original timestamps when copying records
|
|
@@ -542,7 +553,7 @@ const latest50 = await userRepo.findRecentlyCreated(50)
|
|
|
542
553
|
|
|
543
554
|
// Get latest user
|
|
544
555
|
const latestUser = await userRepo.findRecentlyCreated(1)
|
|
545
|
-
console.log(latestUser[0])
|
|
556
|
+
console.log(latestUser[0]) // Most recent user
|
|
546
557
|
```
|
|
547
558
|
|
|
548
559
|
#### findRecentlyUpdated
|
|
@@ -605,15 +616,16 @@ const users = await userRepo.createMany([
|
|
|
605
616
|
|
|
606
617
|
// All records created with the same timestamp
|
|
607
618
|
users.forEach(user => {
|
|
608
|
-
console.log(user.created_at)
|
|
619
|
+
console.log(user.created_at) // Same timestamp for all
|
|
609
620
|
})
|
|
610
621
|
|
|
611
622
|
// Empty arrays are handled gracefully
|
|
612
623
|
const empty = await userRepo.createMany([])
|
|
613
|
-
console.log(empty.length)
|
|
624
|
+
console.log(empty.length) // 0
|
|
614
625
|
```
|
|
615
626
|
|
|
616
627
|
**Performance Benefits:**
|
|
628
|
+
|
|
617
629
|
- Single database roundtrip instead of N queries
|
|
618
630
|
- All records get the same timestamp (consistent)
|
|
619
631
|
- ~10-100x faster than individual creates for large batches
|
|
@@ -646,8 +658,8 @@ const updated = await userRepo.updateMany(userIds, {
|
|
|
646
658
|
|
|
647
659
|
// All records updated with same timestamp
|
|
648
660
|
updated.forEach(user => {
|
|
649
|
-
console.log(user.status)
|
|
650
|
-
console.log(user.updated_at)
|
|
661
|
+
console.log(user.status) // 'active'
|
|
662
|
+
console.log(user.updated_at) // Same timestamp for all
|
|
651
663
|
})
|
|
652
664
|
|
|
653
665
|
// Works with string IDs too
|
|
@@ -656,10 +668,11 @@ await userRepo.updateMany(stringIds, { verified: true })
|
|
|
656
668
|
|
|
657
669
|
// Empty arrays are handled gracefully
|
|
658
670
|
const empty = await userRepo.updateMany([], { status: 'inactive' })
|
|
659
|
-
console.log(empty.length)
|
|
671
|
+
console.log(empty.length) // 0
|
|
660
672
|
```
|
|
661
673
|
|
|
662
674
|
**Performance Benefits:**
|
|
675
|
+
|
|
663
676
|
- Single UPDATE query with WHERE id IN clause
|
|
664
677
|
- Single SELECT to fetch updated records
|
|
665
678
|
- Much faster than individual updates
|
|
@@ -692,21 +705,18 @@ const userIds = [1, 2, 3, 4, 5]
|
|
|
692
705
|
await userRepo.touchMany(userIds)
|
|
693
706
|
|
|
694
707
|
// Verify all were touched
|
|
695
|
-
const touched = await db
|
|
696
|
-
.selectFrom('users')
|
|
697
|
-
.selectAll()
|
|
698
|
-
.where('id', 'in', userIds)
|
|
699
|
-
.execute()
|
|
708
|
+
const touched = await db.selectFrom('users').selectAll().where('id', 'in', userIds).execute()
|
|
700
709
|
|
|
701
710
|
touched.forEach(user => {
|
|
702
|
-
console.log(user.updated_at)
|
|
711
|
+
console.log(user.updated_at) // All have same new timestamp
|
|
703
712
|
})
|
|
704
713
|
|
|
705
714
|
// Empty arrays are handled gracefully
|
|
706
|
-
await userRepo.touchMany([])
|
|
715
|
+
await userRepo.touchMany([]) // No-op
|
|
707
716
|
```
|
|
708
717
|
|
|
709
718
|
**Performance Benefits:**
|
|
719
|
+
|
|
710
720
|
- Single UPDATE query setting only timestamp column
|
|
711
721
|
- No data fetched or returned
|
|
712
722
|
- Extremely fast even for large batches
|
|
@@ -821,20 +831,24 @@ console.log(columns)
|
|
|
821
831
|
|
|
822
832
|
### Multiple Plugins
|
|
823
833
|
|
|
824
|
-
Combine timestamps with other plugins:
|
|
834
|
+
Combine timestamps with other plugins via @kysera/executor's Unified Execution Layer:
|
|
825
835
|
|
|
826
836
|
```typescript
|
|
837
|
+
import { createExecutor } from '@kysera/executor'
|
|
827
838
|
import { timestampsPlugin } from '@kysera/timestamps'
|
|
828
839
|
import { softDeletePlugin } from '@kysera/soft-delete'
|
|
829
840
|
import { auditPlugin } from '@kysera/audit'
|
|
830
841
|
|
|
831
|
-
|
|
842
|
+
// Register all plugins with Unified Execution Layer
|
|
843
|
+
const executor = await createExecutor(db, [
|
|
832
844
|
timestampsPlugin(),
|
|
833
845
|
softDeletePlugin(),
|
|
834
|
-
auditPlugin({
|
|
846
|
+
auditPlugin({ getUserId: () => currentUserId })
|
|
835
847
|
])
|
|
836
848
|
|
|
837
|
-
|
|
849
|
+
const orm = await createORM(executor, [])
|
|
850
|
+
|
|
851
|
+
// All plugins work together seamlessly:
|
|
838
852
|
const user = await userRepo.create({ email: 'test@example.com', name: 'Test' })
|
|
839
853
|
// ✅ created_at added by timestamps plugin
|
|
840
854
|
// ✅ deleted_at set to null by soft-delete plugin
|
|
@@ -846,7 +860,7 @@ const user = await userRepo.create({ email: 'test@example.com', name: 'Test' })
|
|
|
846
860
|
Timestamps work seamlessly with transactions:
|
|
847
861
|
|
|
848
862
|
```typescript
|
|
849
|
-
await db.transaction().execute(async
|
|
863
|
+
await db.transaction().execute(async trx => {
|
|
850
864
|
const txRepo = userRepo.withTransaction(trx)
|
|
851
865
|
|
|
852
866
|
const user = await txRepo.create({
|
|
@@ -897,11 +911,11 @@ function createUserRepository(executor: Executor<Database>) {
|
|
|
897
911
|
const factory = createRepositoryFactory(executor)
|
|
898
912
|
return factory.create<'users', User>({
|
|
899
913
|
tableName: 'users',
|
|
900
|
-
mapRow:
|
|
914
|
+
mapRow: row => ({
|
|
901
915
|
id: row.id,
|
|
902
916
|
email: row.email,
|
|
903
917
|
name: row.name,
|
|
904
|
-
createdAt: new Date(row.created_at),
|
|
918
|
+
createdAt: new Date(row.created_at), // Convert to Date
|
|
905
919
|
updatedAt: row.updated_at ? new Date(row.updated_at) : null
|
|
906
920
|
}),
|
|
907
921
|
schemas: {
|
|
@@ -1035,10 +1049,10 @@ const after: User[] = await userRepo.findCreatedAfter('2024-01-01')
|
|
|
1035
1049
|
const between: User[] = await userRepo.findCreatedBetween('2024-01-01', '2024-12-31')
|
|
1036
1050
|
|
|
1037
1051
|
// ❌ Type error: wrong argument type
|
|
1038
|
-
await userRepo.findCreatedAfter(12345)
|
|
1052
|
+
await userRepo.findCreatedAfter(12345) // Error: number not assignable
|
|
1039
1053
|
|
|
1040
1054
|
// ❌ Type error: method doesn't exist
|
|
1041
|
-
await userRepo.nonExistentMethod()
|
|
1055
|
+
await userRepo.nonExistentMethod() // Error: method doesn't exist
|
|
1042
1056
|
```
|
|
1043
1057
|
|
|
1044
1058
|
### Database Schema Types
|
|
@@ -1051,15 +1065,15 @@ interface Database {
|
|
|
1051
1065
|
id: Generated<number>
|
|
1052
1066
|
email: string
|
|
1053
1067
|
name: string
|
|
1054
|
-
created_at: Generated<Date>
|
|
1055
|
-
updated_at: Date | null
|
|
1068
|
+
created_at: Generated<Date> // Auto-generated
|
|
1069
|
+
updated_at: Date | null // Nullable
|
|
1056
1070
|
}
|
|
1057
1071
|
}
|
|
1058
1072
|
|
|
1059
1073
|
// TypeScript ensures correct column types
|
|
1060
1074
|
const plugin = timestampsPlugin({
|
|
1061
|
-
createdAtColumn: 'created_at',
|
|
1062
|
-
updatedAtColumn: 'updated_at'
|
|
1075
|
+
createdAtColumn: 'created_at', // ✅ Must match schema
|
|
1076
|
+
updatedAtColumn: 'updated_at' // ✅ Must match schema
|
|
1063
1077
|
})
|
|
1064
1078
|
```
|
|
1065
1079
|
|
|
@@ -1075,20 +1089,21 @@ Creates a timestamps plugin instance.
|
|
|
1075
1089
|
|
|
1076
1090
|
```typescript
|
|
1077
1091
|
interface TimestampsOptions {
|
|
1078
|
-
createdAtColumn?: string
|
|
1079
|
-
updatedAtColumn?: string
|
|
1080
|
-
setUpdatedAtOnInsert?: boolean
|
|
1081
|
-
tables?: string[]
|
|
1082
|
-
excludeTables?: string[]
|
|
1083
|
-
getTimestamp?: () => Date | string | number
|
|
1084
|
-
dateFormat?: 'iso' | 'unix' | 'date'
|
|
1085
|
-
primaryKeyColumn?: string
|
|
1092
|
+
createdAtColumn?: string // Default: 'created_at'
|
|
1093
|
+
updatedAtColumn?: string // Default: 'updated_at'
|
|
1094
|
+
setUpdatedAtOnInsert?: boolean // Default: false
|
|
1095
|
+
tables?: string[] // Default: undefined (all tables)
|
|
1096
|
+
excludeTables?: string[] // Default: undefined
|
|
1097
|
+
getTimestamp?: () => Date | string | number // Custom generator
|
|
1098
|
+
dateFormat?: 'iso' | 'unix' | 'date' // Default: 'iso'
|
|
1099
|
+
primaryKeyColumn?: string // Default: 'id'
|
|
1086
1100
|
}
|
|
1087
1101
|
```
|
|
1088
1102
|
|
|
1089
1103
|
**Returns:** `Plugin` instance
|
|
1090
1104
|
|
|
1091
1105
|
**Example:**
|
|
1106
|
+
|
|
1092
1107
|
```typescript
|
|
1093
1108
|
const plugin = timestampsPlugin({
|
|
1094
1109
|
createdAtColumn: 'created',
|
|
@@ -1106,6 +1121,7 @@ const plugin = timestampsPlugin({
|
|
|
1106
1121
|
Find records created after a date.
|
|
1107
1122
|
|
|
1108
1123
|
**Parameters:**
|
|
1124
|
+
|
|
1109
1125
|
- `date: Date | string | number` - Date to compare against
|
|
1110
1126
|
|
|
1111
1127
|
**Returns:** `Promise<T[]>`
|
|
@@ -1117,6 +1133,7 @@ Find records created after a date.
|
|
|
1117
1133
|
Find records created before a date.
|
|
1118
1134
|
|
|
1119
1135
|
**Parameters:**
|
|
1136
|
+
|
|
1120
1137
|
- `date: Date | string | number` - Date to compare against
|
|
1121
1138
|
|
|
1122
1139
|
**Returns:** `Promise<T[]>`
|
|
@@ -1128,6 +1145,7 @@ Find records created before a date.
|
|
|
1128
1145
|
Find records created between two dates (inclusive).
|
|
1129
1146
|
|
|
1130
1147
|
**Parameters:**
|
|
1148
|
+
|
|
1131
1149
|
- `startDate: Date | string | number` - Start date
|
|
1132
1150
|
- `endDate: Date | string | number` - End date
|
|
1133
1151
|
|
|
@@ -1140,6 +1158,7 @@ Find records created between two dates (inclusive).
|
|
|
1140
1158
|
Find records updated after a date.
|
|
1141
1159
|
|
|
1142
1160
|
**Parameters:**
|
|
1161
|
+
|
|
1143
1162
|
- `date: Date | string | number` - Date to compare against
|
|
1144
1163
|
|
|
1145
1164
|
**Returns:** `Promise<T[]>`
|
|
@@ -1151,6 +1170,7 @@ Find records updated after a date.
|
|
|
1151
1170
|
Get most recently created records.
|
|
1152
1171
|
|
|
1153
1172
|
**Parameters:**
|
|
1173
|
+
|
|
1154
1174
|
- `limit?: number` - Number of records (default: 10)
|
|
1155
1175
|
|
|
1156
1176
|
**Returns:** `Promise<T[]>`
|
|
@@ -1162,6 +1182,7 @@ Get most recently created records.
|
|
|
1162
1182
|
Get most recently updated records.
|
|
1163
1183
|
|
|
1164
1184
|
**Parameters:**
|
|
1185
|
+
|
|
1165
1186
|
- `limit?: number` - Number of records (default: 10)
|
|
1166
1187
|
|
|
1167
1188
|
**Returns:** `Promise<T[]>`
|
|
@@ -1173,6 +1194,7 @@ Get most recently updated records.
|
|
|
1173
1194
|
Create a record without adding timestamps.
|
|
1174
1195
|
|
|
1175
1196
|
**Parameters:**
|
|
1197
|
+
|
|
1176
1198
|
- `input: unknown` - Create data
|
|
1177
1199
|
|
|
1178
1200
|
**Returns:** `Promise<T>`
|
|
@@ -1184,6 +1206,7 @@ Create a record without adding timestamps.
|
|
|
1184
1206
|
Update a record without modifying updated_at.
|
|
1185
1207
|
|
|
1186
1208
|
**Parameters:**
|
|
1209
|
+
|
|
1187
1210
|
- `id: number` - Record ID
|
|
1188
1211
|
- `input: unknown` - Update data
|
|
1189
1212
|
|
|
@@ -1196,6 +1219,7 @@ Update a record without modifying updated_at.
|
|
|
1196
1219
|
Update only the updated_at timestamp.
|
|
1197
1220
|
|
|
1198
1221
|
**Parameters:**
|
|
1222
|
+
|
|
1199
1223
|
- `id: number` - Record ID
|
|
1200
1224
|
|
|
1201
1225
|
**Returns:** `Promise<void>`
|
|
@@ -1215,11 +1239,13 @@ Get configured column names.
|
|
|
1215
1239
|
Create multiple records with timestamps in a single bulk INSERT operation.
|
|
1216
1240
|
|
|
1217
1241
|
**Parameters:**
|
|
1242
|
+
|
|
1218
1243
|
- `inputs: unknown[]` - Array of create data objects
|
|
1219
1244
|
|
|
1220
1245
|
**Returns:** `Promise<T[]>` - Array of created records
|
|
1221
1246
|
|
|
1222
1247
|
**Example:**
|
|
1248
|
+
|
|
1223
1249
|
```typescript
|
|
1224
1250
|
const users = await userRepo.createMany([
|
|
1225
1251
|
{ name: 'Alice', email: 'alice@example.com' },
|
|
@@ -1234,12 +1260,14 @@ const users = await userRepo.createMany([
|
|
|
1234
1260
|
Update multiple records with the same data, automatically setting updated_at.
|
|
1235
1261
|
|
|
1236
1262
|
**Parameters:**
|
|
1263
|
+
|
|
1237
1264
|
- `ids: (number | string)[]` - Array of record IDs to update
|
|
1238
1265
|
- `input: unknown` - Update data (applied to all records)
|
|
1239
1266
|
|
|
1240
1267
|
**Returns:** `Promise<T[]>` - Array of updated records
|
|
1241
1268
|
|
|
1242
1269
|
**Example:**
|
|
1270
|
+
|
|
1243
1271
|
```typescript
|
|
1244
1272
|
const updated = await userRepo.updateMany([1, 2, 3], {
|
|
1245
1273
|
status: 'active'
|
|
@@ -1253,11 +1281,13 @@ const updated = await userRepo.updateMany([1, 2, 3], {
|
|
|
1253
1281
|
Update only the updated_at timestamp for multiple records.
|
|
1254
1282
|
|
|
1255
1283
|
**Parameters:**
|
|
1284
|
+
|
|
1256
1285
|
- `ids: (number | string)[]` - Array of record IDs to touch
|
|
1257
1286
|
|
|
1258
1287
|
**Returns:** `Promise<void>`
|
|
1259
1288
|
|
|
1260
1289
|
**Example:**
|
|
1290
|
+
|
|
1261
1291
|
```typescript
|
|
1262
1292
|
await userRepo.touchMany([1, 2, 3, 4, 5])
|
|
1263
1293
|
```
|
|
@@ -1274,14 +1304,14 @@ interface Database {
|
|
|
1274
1304
|
users: {
|
|
1275
1305
|
id: Generated<number>
|
|
1276
1306
|
created_at: Generated<Date>
|
|
1277
|
-
updated_at: Date | null
|
|
1307
|
+
updated_at: Date | null // ✅ Null until first update
|
|
1278
1308
|
}
|
|
1279
1309
|
}
|
|
1280
1310
|
|
|
1281
1311
|
// ❌ Bad: updated_at not nullable
|
|
1282
1312
|
interface Database {
|
|
1283
1313
|
users: {
|
|
1284
|
-
updated_at: Date
|
|
1314
|
+
updated_at: Date // ❌ What value on insert?
|
|
1285
1315
|
}
|
|
1286
1316
|
}
|
|
1287
1317
|
```
|
|
@@ -1292,14 +1322,14 @@ interface Database {
|
|
|
1292
1322
|
// ✅ Good: created_at is generated
|
|
1293
1323
|
interface Database {
|
|
1294
1324
|
users: {
|
|
1295
|
-
created_at: Generated<Date>
|
|
1325
|
+
created_at: Generated<Date> // ✅ Auto-generated
|
|
1296
1326
|
}
|
|
1297
1327
|
}
|
|
1298
1328
|
|
|
1299
1329
|
// ⚠️ OK but verbose: created_at is optional
|
|
1300
1330
|
interface Database {
|
|
1301
1331
|
users: {
|
|
1302
|
-
created_at: Date
|
|
1332
|
+
created_at: Date // Must be provided or plugin adds it
|
|
1303
1333
|
}
|
|
1304
1334
|
}
|
|
1305
1335
|
```
|
|
@@ -1336,12 +1366,12 @@ const plugin = timestampsPlugin({
|
|
|
1336
1366
|
```typescript
|
|
1337
1367
|
// ✅ Good: ISO strings work everywhere
|
|
1338
1368
|
const plugin = timestampsPlugin({
|
|
1339
|
-
dateFormat: 'iso'
|
|
1369
|
+
dateFormat: 'iso' // Portable across databases
|
|
1340
1370
|
})
|
|
1341
1371
|
|
|
1342
1372
|
// ⚠️ OK: Unix timestamps for performance
|
|
1343
1373
|
const plugin = timestampsPlugin({
|
|
1344
|
-
dateFormat: 'unix'
|
|
1374
|
+
dateFormat: 'unix' // Good for INTEGER columns
|
|
1345
1375
|
})
|
|
1346
1376
|
```
|
|
1347
1377
|
|
|
@@ -1362,11 +1392,11 @@ CREATE INDEX idx_users_updated_at ON users(updated_at);
|
|
|
1362
1392
|
|
|
1363
1393
|
```typescript
|
|
1364
1394
|
// ✅ Good: Lightweight activity tracking
|
|
1365
|
-
await userRepo.touch(userId)
|
|
1395
|
+
await userRepo.touch(userId) // Only updates timestamp
|
|
1366
1396
|
|
|
1367
1397
|
// ❌ Bad: Unnecessary data transfer
|
|
1368
1398
|
const user = await userRepo.findById(userId)
|
|
1369
|
-
await userRepo.update(userId, user)
|
|
1399
|
+
await userRepo.update(userId, user) // Fetches + updates all fields
|
|
1370
1400
|
```
|
|
1371
1401
|
|
|
1372
1402
|
### 8. Use Batch Operations for Multiple Records
|
|
@@ -1413,20 +1443,20 @@ for (const id of [1, 2, 3, 4, 5]) {
|
|
|
1413
1443
|
|
|
1414
1444
|
### Plugin Overhead
|
|
1415
1445
|
|
|
1416
|
-
| Operation
|
|
1417
|
-
|
|
1418
|
-
| **create**
|
|
1419
|
-
| **update**
|
|
1420
|
-
| **findById**
|
|
1421
|
-
| **findRecentlyCreated** | -
|
|
1446
|
+
| Operation | Base | With Timestamps | Overhead |
|
|
1447
|
+
| ----------------------- | ---- | --------------- | -------- |
|
|
1448
|
+
| **create** | 2ms | 2.05ms | +0.05ms |
|
|
1449
|
+
| **update** | 2ms | 2.05ms | +0.05ms |
|
|
1450
|
+
| **findById** | 1ms | 1ms | 0ms |
|
|
1451
|
+
| **findRecentlyCreated** | - | 5ms | N/A |
|
|
1422
1452
|
|
|
1423
1453
|
### Timestamp Format Performance
|
|
1424
1454
|
|
|
1425
|
-
| Format
|
|
1426
|
-
|
|
1427
|
-
| **ISO**
|
|
1428
|
-
| **Unix** | ~0.0005ms
|
|
1429
|
-
| **Date** | ~0.001ms
|
|
1455
|
+
| Format | Generation Time | Storage Size | Query Performance |
|
|
1456
|
+
| -------- | --------------- | ------------ | ----------------- |
|
|
1457
|
+
| **ISO** | ~0.001ms | 24-27 bytes | Medium |
|
|
1458
|
+
| **Unix** | ~0.0005ms | 4-8 bytes | Fast |
|
|
1459
|
+
| **Date** | ~0.001ms | Varies | Medium |
|
|
1430
1460
|
|
|
1431
1461
|
**Recommendation:** Use `unix` for high-performance time-series data, `iso` for general use.
|
|
1432
1462
|
|
|
@@ -1468,6 +1498,7 @@ const sorted = all.sort((a, b) => b.createdAt - a.createdAt)
|
|
|
1468
1498
|
**Solutions:**
|
|
1469
1499
|
|
|
1470
1500
|
1. **Check plugin is registered:**
|
|
1501
|
+
|
|
1471
1502
|
```typescript
|
|
1472
1503
|
// ❌ Plugin not registered
|
|
1473
1504
|
const orm = await createORM(db, [])
|
|
@@ -1477,19 +1508,21 @@ const orm = await createORM(db, [timestampsPlugin()])
|
|
|
1477
1508
|
```
|
|
1478
1509
|
|
|
1479
1510
|
2. **Check table is not excluded:**
|
|
1511
|
+
|
|
1480
1512
|
```typescript
|
|
1481
1513
|
// Check configuration
|
|
1482
1514
|
const plugin = timestampsPlugin({
|
|
1483
|
-
excludeTables: ['users']
|
|
1515
|
+
excludeTables: ['users'] // ❌ Users excluded!
|
|
1484
1516
|
})
|
|
1485
1517
|
|
|
1486
1518
|
// Fix: Remove from exclusions
|
|
1487
1519
|
const plugin = timestampsPlugin({
|
|
1488
|
-
excludeTables: []
|
|
1520
|
+
excludeTables: [] // ✅ No exclusions
|
|
1489
1521
|
})
|
|
1490
1522
|
```
|
|
1491
1523
|
|
|
1492
1524
|
3. **Check column exists in database:**
|
|
1525
|
+
|
|
1493
1526
|
```sql
|
|
1494
1527
|
-- Check schema
|
|
1495
1528
|
DESCRIBE users;
|
|
@@ -1507,17 +1540,17 @@ WHERE table_name = 'users';
|
|
|
1507
1540
|
```typescript
|
|
1508
1541
|
// For TIMESTAMP/DATETIME columns
|
|
1509
1542
|
const plugin = timestampsPlugin({
|
|
1510
|
-
dateFormat: 'iso'
|
|
1543
|
+
dateFormat: 'iso' // ✅ ISO string
|
|
1511
1544
|
})
|
|
1512
1545
|
|
|
1513
1546
|
// For INTEGER columns
|
|
1514
1547
|
const plugin = timestampsPlugin({
|
|
1515
|
-
dateFormat: 'unix'
|
|
1548
|
+
dateFormat: 'unix' // ✅ Unix timestamp
|
|
1516
1549
|
})
|
|
1517
1550
|
|
|
1518
1551
|
// For DATE columns
|
|
1519
1552
|
const plugin = timestampsPlugin({
|
|
1520
|
-
dateFormat: 'date'
|
|
1553
|
+
dateFormat: 'date' // ✅ Date object
|
|
1521
1554
|
})
|
|
1522
1555
|
```
|
|
1523
1556
|
|
|
@@ -1531,15 +1564,15 @@ const plugin = timestampsPlugin({
|
|
|
1531
1564
|
// ❌ Wrong: Direct factory (no plugin extensions)
|
|
1532
1565
|
const factory = createRepositoryFactory(db)
|
|
1533
1566
|
const userRepo = factory.create(/* ... */)
|
|
1534
|
-
await userRepo.findRecentlyCreated()
|
|
1567
|
+
await userRepo.findRecentlyCreated() // ❌ Undefined
|
|
1535
1568
|
|
|
1536
1569
|
// ✅ Correct: Plugin container with plugins
|
|
1537
1570
|
const orm = await createORM(db, [timestampsPlugin()])
|
|
1538
|
-
const userRepo = orm.createRepository(
|
|
1571
|
+
const userRepo = orm.createRepository(executor => {
|
|
1539
1572
|
const factory = createRepositoryFactory(executor)
|
|
1540
1573
|
return factory.create(/* ... */)
|
|
1541
1574
|
})
|
|
1542
|
-
await userRepo.findRecentlyCreated()
|
|
1575
|
+
await userRepo.findRecentlyCreated() // ✅ Works!
|
|
1543
1576
|
```
|
|
1544
1577
|
|
|
1545
1578
|
### Timestamps in Transactions
|
|
@@ -1549,13 +1582,17 @@ await userRepo.findRecentlyCreated() // ✅ Works!
|
|
|
1549
1582
|
**Solution:** Use `withTransaction`:
|
|
1550
1583
|
|
|
1551
1584
|
```typescript
|
|
1552
|
-
await db.transaction().execute(async
|
|
1585
|
+
await db.transaction().execute(async trx => {
|
|
1553
1586
|
// ❌ Wrong: Using original repo
|
|
1554
|
-
await userRepo.create({
|
|
1587
|
+
await userRepo.create({
|
|
1588
|
+
/* ... */
|
|
1589
|
+
})
|
|
1555
1590
|
|
|
1556
1591
|
// ✅ Correct: Use transaction repo
|
|
1557
1592
|
const txRepo = userRepo.withTransaction(trx)
|
|
1558
|
-
await txRepo.create({
|
|
1593
|
+
await txRepo.create({
|
|
1594
|
+
/* ... */
|
|
1595
|
+
}) // ✅ Timestamps added
|
|
1559
1596
|
})
|
|
1560
1597
|
```
|
|
1561
1598
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Plugin } from '@kysera/executor';
|
|
2
|
+
import { Repository } from '@kysera/repository';
|
|
2
3
|
import { KyseraLogger } from '@kysera/core';
|
|
3
4
|
import { z } from 'zod';
|
|
4
5
|
|
|
@@ -108,6 +109,7 @@ type TimestampsRepository<Entity, DB> = Repository<Entity, DB> & TimestampMethod
|
|
|
108
109
|
* - Configurable timestamp format (ISO, Unix, Date)
|
|
109
110
|
* - Query helpers: findCreatedAfter, findUpdatedAfter, etc.
|
|
110
111
|
* - Bulk operations: createMany, updateMany, touchMany
|
|
112
|
+
* - **Cross-database support**: Works with PostgreSQL, MySQL, SQLite, and MSSQL
|
|
111
113
|
*
|
|
112
114
|
* ## Transaction Behavior
|
|
113
115
|
*
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {silentLogger}from'@kysera/core';import {z}from'zod';var
|
|
1
|
+
import {silentLogger,detectDialect}from'@kysera/core';import {z}from'zod';var h="__VERSION__",v=h.startsWith("__")?"0.0.0-dev":h;var B=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()});function p(s){if(s.getTimestamp)return s.getTimestamp();let n=new Date;switch(s.dateFormat){case "unix":return Math.floor(n.getTime()/1e3);case "date":return n;case "iso":default:return n.toISOString()}}function A(s,n){return n.excludeTables?.includes(s)?false:n.tables?n.tables.includes(s):true}function w(s,n,i){return {select(){return s.selectFrom(n)},where(y,d){return s.selectFrom(n).where(i,y,d)}}}function N(s){let n=detectDialect(s);return n!=="mysql"&&n!=="mssql"}var O=(s={})=>{let{createdAtColumn:n="created_at",updatedAtColumn:i="updated_at",setUpdatedAtOnInsert:y=false,primaryKeyColumn:d="id",logger:u=silentLogger}=s;return {name:"@kysera/timestamps",version:v,priority:50,onInit(){},async onDestroy(){u.debug("Timestamps plugin destroyed");},extendRepository(g){if(!("tableName"in g)||!("executor"in g))return g;let t=g;if(!A(t.tableName,s))return u.debug(`Table ${t.tableName} excluded from timestamps, skipping extension`),g;u.debug(`Extending repository for table ${t.tableName} with timestamp methods`);let T=t.create.bind(t),f=t.update.bind(t),o=t.executor;return {...t,async create(e){let r=e,a=p(s),m={...r,[n]:r[n]??a};return y&&(m[i]=r[i]??a),u.debug(`Creating record in ${t.tableName} with timestamp ${a}`),await T(m)},async update(e,r){let a=r,m=p(s),c={...a,[i]:a[i]??m};return u.debug(`Updating record ${e} in ${t.tableName} with timestamp ${m}`),await f(e,c)},async findCreatedAfter(e){return await w(o,t.tableName,n).where(">",String(e)).selectAll().execute()},async findCreatedBefore(e){return await w(o,t.tableName,n).where("<",String(e)).selectAll().execute()},async findCreatedBetween(e,r){return await o.selectFrom(t.tableName).selectAll().where(n,">=",e).where(n,"<=",r).execute()},async findUpdatedAfter(e){return await w(o,t.tableName,i).where(">",String(e)).selectAll().execute()},async findRecentlyUpdated(e=10){return await o.selectFrom(t.tableName).selectAll().orderBy(i,"desc").limit(e).execute()},async findRecentlyCreated(e=10){return await o.selectFrom(t.tableName).selectAll().orderBy(n,"desc").limit(e).execute()},async createWithoutTimestamps(e){return u.debug(`Creating record in ${t.tableName} without timestamps`),await T(e)},async updateWithoutTimestamp(e,r){return u.debug(`Updating record ${e} in ${t.tableName} without timestamp`),await f(e,r)},async touch(e){let r=p(s),a={[i]:r};u.info(`Touching record ${e} in ${t.tableName}`),await o.updateTable(t.tableName).set(a).where(d,"=",e).execute();},getTimestampColumns(){return {createdAt:n,updatedAt:i}},async createMany(e){if(!e||e.length===0)return [];let r=p(s),a=e.map(m=>{let c=m,b={...c,[n]:c[n]??r};return y&&(b[i]=c[i]??r),b});if(u.info(`Creating ${e.length} records in ${t.tableName} with timestamp ${r}`),N(o))return await o.insertInto(t.tableName).values(a).returningAll().execute();{await o.insertInto(t.tableName).values(a).execute();let m=a.length;return (await o.selectFrom(t.tableName).selectAll().where(n,"=",r).orderBy(d,"desc").limit(m).execute()).reverse()}},async updateMany(e,r){if(!e||e.length===0)return [];let a=r,m=p(s),c={...a,[i]:a[i]??m};return u.info(`Updating ${e.length} records in ${t.tableName} with timestamp ${m}`),await o.updateTable(t.tableName).set(c).where(d,"in",e).execute(),await o.selectFrom(t.tableName).selectAll().where(d,"in",e).execute()},async touchMany(e){if(!e||e.length===0)return;let r=p(s),a={[i]:r};u.info(`Touching ${e.length} records in ${t.tableName}`),await o.updateTable(t.tableName).set(a).where(d,"in",e).execute();}}}}};export{B as TimestampsOptionsSchema,O as timestampsPlugin};//# sourceMappingURL=index.js.map
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":["TimestampsOptionsSchema","z","getTimestamp","options","now","shouldApplyTimestamps","tableName","createTimestampQuery","executor","column","operator","value","timestampsPlugin","createdAtColumn","updatedAtColumn","setUpdatedAtOnInsert","primaryKeyColumn","logger","silentLogger","qb","_context","repo","baseRepo","originalCreate","originalUpdate","input","data","timestamp","dataWithTimestamps","id","dataWithTimestamp","date","startDate","endDate","limit","updateData","inputs","result","ids"],"mappings":"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","file":"index.js","sourcesContent":["import type { Plugin, Repository } from '@kysera/repository';\nimport type { Kysely, SelectQueryBuilder } from 'kysely';\nimport { silentLogger } from '@kysera/core';\nimport type { KyseraLogger } from '@kysera/core';\nimport { z } from 'zod';\n\n/**\n * Database schema with timestamp columns\n */\ntype TimestampedTable = Record<string, unknown>;\n\n/**\n * Timestamp methods added to repositories\n */\nexport interface TimestampMethods<T> {\n findCreatedAfter(date: Date | string): Promise<T[]>;\n findCreatedBefore(date: Date | string): Promise<T[]>;\n findCreatedBetween(startDate: Date | string, endDate: Date | string): Promise<T[]>;\n findUpdatedAfter(date: Date | string): Promise<T[]>;\n findRecentlyUpdated(limit?: number): Promise<T[]>;\n findRecentlyCreated(limit?: number): Promise<T[]>;\n createWithoutTimestamps(input: unknown): Promise<T>;\n updateWithoutTimestamp(id: number, input: unknown): Promise<T>;\n touch(id: number): Promise<void>;\n getTimestampColumns(): { createdAt: string; updatedAt: string };\n createMany(inputs: unknown[]): Promise<T[]>;\n updateMany(ids: (number | string)[], input: unknown): Promise<T[]>;\n touchMany(ids: (number | string)[]): Promise<void>;\n}\n\n/**\n * Options for the timestamps plugin\n */\nexport interface TimestampsOptions {\n /**\n * Name of the created_at column\n * @default 'created_at'\n */\n createdAtColumn?: string;\n\n /**\n * Name of the updated_at column\n * @default 'updated_at'\n */\n updatedAtColumn?: string;\n\n /**\n * Whether to set updated_at on insert\n * @default false\n */\n setUpdatedAtOnInsert?: boolean;\n\n /**\n * List of tables that should have timestamps\n * If not specified, all tables will have timestamps\n */\n tables?: string[];\n\n /**\n * Tables that should be excluded from timestamps\n */\n excludeTables?: string[];\n\n /**\n * Custom timestamp function (defaults to new Date().toISOString())\n */\n getTimestamp?: () => Date | string | number;\n\n /**\n * Date format for database (ISO string by default)\n */\n dateFormat?: 'iso' | 'unix' | 'date';\n\n /**\n * Name of the primary key column used by touch() method\n * @default 'id'\n */\n primaryKeyColumn?: string;\n\n /**\n * Logger for plugin operations.\n * Uses KyseraLogger interface from @kysera/core.\n *\n * @default silentLogger (no output)\n */\n logger?: KyseraLogger;\n}\n\n/**\n * Zod schema for TimestampsOptions\n * Used for validation and configuration in the kysera-cli\n */\nexport const TimestampsOptionsSchema = z.object({\n createdAtColumn: z.string().optional(),\n updatedAtColumn: z.string().optional(),\n setUpdatedAtOnInsert: z.boolean().optional(),\n tables: z.array(z.string()).optional(),\n excludeTables: z.array(z.string()).optional(),\n getTimestamp: z.function().optional(),\n dateFormat: z.enum(['iso', 'unix', 'date']).optional(),\n primaryKeyColumn: z.string().optional(),\n});\n\n/**\n * Repository extended with timestamp methods\n */\nexport type TimestampsRepository<Entity, DB> = Repository<Entity, DB> & TimestampMethods<Entity>;\n\n/**\n * Get the current timestamp based on options\n */\nfunction getTimestamp(options: TimestampsOptions): Date | string | number {\n if (options.getTimestamp) {\n return options.getTimestamp();\n }\n\n const now = new Date();\n\n switch (options.dateFormat) {\n case 'unix':\n return Math.floor(now.getTime() / 1000);\n case 'date':\n return now;\n case 'iso':\n default:\n return now.toISOString();\n }\n}\n\n/**\n * Check if a table should have timestamps\n */\nfunction shouldApplyTimestamps(tableName: string, options: TimestampsOptions): boolean {\n if (options.excludeTables?.includes(tableName)) {\n return false;\n }\n\n if (options.tables) {\n return options.tables.includes(tableName);\n }\n\n return true;\n}\n\n/**\n * Type-safe query builder for timestamp operations\n */\nfunction createTimestampQuery(\n executor: Kysely<Record<string, TimestampedTable>>,\n tableName: string,\n column: string\n): {\n select(): SelectQueryBuilder<Record<string, TimestampedTable>, typeof tableName, {}>;\n where<V>(operator: string, value: V): SelectQueryBuilder<Record<string, TimestampedTable>, typeof tableName, {}>;\n} {\n return {\n select() {\n return executor.selectFrom(tableName as never);\n },\n where<V>(operator: string, value: V) {\n return executor.selectFrom(tableName as never).where(column as never, operator as never, value as never);\n },\n };\n}\n\n/**\n * Timestamps Plugin\n *\n * Automatically manages created_at and updated_at timestamps for database records.\n * Works by overriding repository methods to add timestamp values.\n *\n * ## Features\n *\n * - Automatic `created_at` on insert\n * - Automatic `updated_at` on every update\n * - Configurable column names\n * - Configurable timestamp format (ISO, Unix, Date)\n * - Query helpers: findCreatedAfter, findUpdatedAfter, etc.\n * - Bulk operations: createMany, updateMany, touchMany\n *\n * ## Transaction Behavior\n *\n * **IMPORTANT**: Timestamp operations respect ACID properties and work correctly with transactions:\n *\n * - ✅ **Commits with transaction**: Timestamps are set using the same executor as the\n * repository operation, so they commit together\n * - ✅ **Rolls back with transaction**: If a transaction is rolled back, all timestamp\n * changes are also rolled back\n * - ✅ **Consistent timestamps**: All operations within a transaction can use the same\n * timestamp by providing a custom `getTimestamp` function\n *\n * ### Correct Transaction Usage\n *\n * ```typescript\n * // ✅ CORRECT: Timestamps are part of transaction\n * await db.transaction().execute(async (trx) => {\n * const repos = createRepositories(trx) // Use transaction executor\n * await repos.users.create({ email: 'test@example.com' }) // created_at auto-set\n * await repos.posts.createMany([...]) // All created_at set consistently\n * // If transaction rolls back, all changes including timestamps roll back\n * })\n * ```\n *\n * ### Consistent Timestamps Across Operations\n *\n * ```typescript\n * // Use shared timestamp for all operations in a transaction\n * const now = new Date()\n * const timestampsWithFixedTime = timestampsPlugin({\n * getTimestamp: () => now\n * })\n *\n * await db.transaction().execute(async (trx) => {\n * // All operations will have the exact same timestamp\n * await repos.users.create(...) // created_at = now\n * await repos.posts.update(...) // updated_at = now\n * })\n * ```\n *\n * @example\n * ```typescript\n * import { timestampsPlugin } from '@kysera/timestamps'\n *\n * const plugin = timestampsPlugin({\n * createdAtColumn: 'created_at',\n * updatedAtColumn: 'updated_at',\n * tables: ['users', 'posts', 'comments']\n * })\n *\n * const orm = createORM(db, [plugin])\n * ```\n */\nexport const timestampsPlugin = (options: TimestampsOptions = {}): Plugin => {\n const {\n createdAtColumn = 'created_at',\n updatedAtColumn = 'updated_at',\n setUpdatedAtOnInsert = false,\n primaryKeyColumn = 'id',\n logger = silentLogger,\n } = options;\n\n return {\n name: '@kysera/timestamps',\n version: '0.5.1',\n\n interceptQuery(qb, _context) {\n // The interceptQuery method can't modify INSERT/UPDATE values in Kysely\n // We handle timestamps through repository method overrides instead\n return qb;\n },\n\n extendRepository<T extends object>(repo: T): T {\n // Check if it's actually a repository (has required properties)\n if (!('tableName' in repo) || !('executor' in repo)) {\n return repo;\n }\n\n // Type assertion is safe here as we've checked for properties\n const baseRepo = repo as T & { tableName: string; executor: unknown; create: Function; update: Function };\n\n // Skip if table doesn't support timestamps\n if (!shouldApplyTimestamps(baseRepo.tableName, options)) {\n logger.debug(`Table ${baseRepo.tableName} excluded from timestamps, skipping extension`);\n return repo;\n }\n\n logger.debug(`Extending repository for table ${baseRepo.tableName} with timestamp methods`);\n\n // Save original methods\n const originalCreate = baseRepo.create.bind(baseRepo);\n const originalUpdate = baseRepo.update.bind(baseRepo);\n const executor = baseRepo.executor as Kysely<Record<string, TimestampedTable>>;\n\n const extendedRepo = {\n ...baseRepo,\n\n // Override create to add timestamps\n async create(input: unknown): Promise<unknown> {\n const data = input as Record<string, unknown>;\n const timestamp = getTimestamp(options);\n const dataWithTimestamps: Record<string, unknown> = {\n ...data,\n [createdAtColumn]: data[createdAtColumn] ?? timestamp,\n };\n\n if (setUpdatedAtOnInsert) {\n dataWithTimestamps[updatedAtColumn] = data[updatedAtColumn] ?? timestamp;\n }\n\n logger.debug(`Creating record in ${baseRepo.tableName} with timestamp ${timestamp}`);\n return await originalCreate(dataWithTimestamps);\n },\n\n // Override update to set updated_at\n async update(id: number, input: unknown): Promise<unknown> {\n const data = input as Record<string, unknown>;\n const timestamp = getTimestamp(options);\n const dataWithTimestamp: Record<string, unknown> = {\n ...data,\n [updatedAtColumn]: data[updatedAtColumn] ?? timestamp,\n };\n\n logger.debug(`Updating record ${id} in ${baseRepo.tableName} with timestamp ${timestamp}`);\n return await originalUpdate(id, dataWithTimestamp);\n },\n\n /**\n * Find records created after a specific date\n */\n async findCreatedAfter(date: Date | string | number): Promise<unknown[]> {\n const query = createTimestampQuery(executor, baseRepo.tableName, createdAtColumn);\n const result = await query.where('>', String(date)).selectAll().execute();\n return result;\n },\n\n /**\n * Find records created before a specific date\n */\n async findCreatedBefore(date: Date | string | number): Promise<unknown[]> {\n const query = createTimestampQuery(executor, baseRepo.tableName, createdAtColumn);\n const result = await query.where('<', String(date)).selectAll().execute();\n return result;\n },\n\n /**\n * Find records created between two dates\n */\n async findCreatedBetween(\n startDate: Date | string | number,\n endDate: Date | string | number\n ): Promise<unknown[]> {\n const result = await executor\n .selectFrom(baseRepo.tableName as never)\n .selectAll()\n .where(createdAtColumn as never, '>=', startDate as never)\n .where(createdAtColumn as never, '<=', endDate as never)\n .execute();\n return result;\n },\n\n /**\n * Find records updated after a specific date\n */\n async findUpdatedAfter(date: Date | string | number): Promise<unknown[]> {\n const query = createTimestampQuery(executor, baseRepo.tableName, updatedAtColumn);\n const result = await query.where('>', String(date)).selectAll().execute();\n return result;\n },\n\n /**\n * Find recently updated records\n */\n async findRecentlyUpdated(limit = 10): Promise<unknown[]> {\n const result = await executor\n .selectFrom(baseRepo.tableName as never)\n .selectAll()\n .orderBy(updatedAtColumn as never, 'desc')\n .limit(limit)\n .execute();\n return result;\n },\n\n /**\n * Find recently created records\n */\n async findRecentlyCreated(limit = 10): Promise<unknown[]> {\n const result = await executor\n .selectFrom(baseRepo.tableName as never)\n .selectAll()\n .orderBy(createdAtColumn as never, 'desc')\n .limit(limit)\n .execute();\n return result;\n },\n\n /**\n * Create without adding timestamps\n */\n async createWithoutTimestamps(input: unknown): Promise<unknown> {\n logger.debug(`Creating record in ${baseRepo.tableName} without timestamps`);\n return await originalCreate(input);\n },\n\n /**\n * Update without modifying timestamp\n */\n async updateWithoutTimestamp(id: number, input: unknown): Promise<unknown> {\n logger.debug(`Updating record ${id} in ${baseRepo.tableName} without timestamp`);\n return await originalUpdate(id, input);\n },\n\n /**\n * Touch a record (update its timestamp)\n */\n async touch(id: number): Promise<void> {\n const timestamp = getTimestamp(options);\n const updateData = { [updatedAtColumn]: timestamp };\n\n logger.info(`Touching record ${id} in ${baseRepo.tableName}`);\n await executor\n .updateTable(baseRepo.tableName as never)\n .set(updateData as never)\n .where(primaryKeyColumn as never, '=', id as never)\n .execute();\n },\n\n /**\n * Get the timestamp column names\n */\n getTimestampColumns(): { createdAt: string; updatedAt: string } {\n return {\n createdAt: createdAtColumn,\n updatedAt: updatedAtColumn,\n };\n },\n\n /**\n * Create multiple records with timestamps\n * Uses efficient bulk INSERT with automatic timestamp injection\n */\n async createMany(inputs: unknown[]): Promise<unknown[]> {\n // Handle empty arrays gracefully\n if (!inputs || inputs.length === 0) {\n return [];\n }\n\n const timestamp = getTimestamp(options);\n const dataWithTimestamps = inputs.map((input) => {\n const data = input as Record<string, unknown>;\n const result: Record<string, unknown> = {\n ...data,\n [createdAtColumn]: data[createdAtColumn] ?? timestamp,\n };\n\n if (setUpdatedAtOnInsert) {\n result[updatedAtColumn] = data[updatedAtColumn] ?? timestamp;\n }\n\n return result;\n });\n\n logger.info(`Creating ${inputs.length} records in ${baseRepo.tableName} with timestamp ${timestamp}`);\n\n // Use Kysely's insertInto for efficient bulk insert\n const result = await executor\n .insertInto(baseRepo.tableName as never)\n .values(dataWithTimestamps as never)\n .returningAll()\n .execute();\n\n return result;\n },\n\n /**\n * Update multiple records (sets updated_at for all)\n * Updates all specified records with the same data\n */\n async updateMany(ids: (number | string)[], input: unknown): Promise<unknown[]> {\n // Handle empty arrays gracefully\n if (!ids || ids.length === 0) {\n return [];\n }\n\n const data = input as Record<string, unknown>;\n const timestamp = getTimestamp(options);\n const dataWithTimestamp: Record<string, unknown> = {\n ...data,\n [updatedAtColumn]: data[updatedAtColumn] ?? timestamp,\n };\n\n logger.info(`Updating ${ids.length} records in ${baseRepo.tableName} with timestamp ${timestamp}`);\n\n // Use Kysely's update with IN clause for efficient bulk update\n await executor\n .updateTable(baseRepo.tableName as never)\n .set(dataWithTimestamp as never)\n .where('id' as never, 'in', ids as never)\n .execute();\n\n // Fetch and return updated records\n const result = await executor\n .selectFrom(baseRepo.tableName as never)\n .selectAll()\n .where('id' as never, 'in', ids as never)\n .execute();\n\n return result;\n },\n\n /**\n * Touch multiple records (update updated_at only)\n * Efficiently updates only the timestamp column\n */\n async touchMany(ids: (number | string)[]): Promise<void> {\n // Handle empty arrays gracefully\n if (!ids || ids.length === 0) {\n return;\n }\n\n const timestamp = getTimestamp(options);\n const updateData = { [updatedAtColumn]: timestamp };\n\n logger.info(`Touching ${ids.length} records in ${baseRepo.tableName}`);\n await executor\n .updateTable(baseRepo.tableName as never)\n .set(updateData as never)\n .where('id' as never, 'in', ids as never)\n .execute();\n },\n };\n\n return extendedRepo as T;\n },\n };\n};\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/version.ts","../src/index.ts"],"names":["RAW_VERSION","VERSION","TimestampsOptionsSchema","z","getTimestamp","options","now","shouldApplyTimestamps","tableName","createTimestampQuery","executor","column","operator","value","supportsReturning","dialect","detectDialect","timestampsPlugin","createdAtColumn","updatedAtColumn","setUpdatedAtOnInsert","primaryKeyColumn","logger","silentLogger","repo","baseRepo","originalCreate","originalUpdate","input","data","timestamp","dataWithTimestamps","id","dataWithTimestamp","date","startDate","endDate","limit","updateData","inputs","result","insertedCount","ids"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Package version - injected at build time by tsup\n * Falls back to development version if not replaced\n * @internal\n */\nconst RAW_VERSION = '__VERSION__'\nexport const VERSION = RAW_VERSION.startsWith('__') ? '0.0.0-dev' : RAW_VERSION\n","import type { Plugin } from '@kysera/executor'\nimport type { Repository } from '@kysera/repository'\nimport type { Kysely, SelectQueryBuilder } from 'kysely'\nimport { silentLogger, detectDialect } from '@kysera/core'\nimport type { KyseraLogger } from '@kysera/core'\nimport { z } from 'zod'\nimport { VERSION } from './version.js'\n\n/**\n * Database schema with timestamp columns\n */\ntype TimestampedTable = Record<string, unknown>\n\n/**\n * Timestamp methods added to repositories\n */\nexport interface TimestampMethods<T> {\n findCreatedAfter(date: Date | string): Promise<T[]>\n findCreatedBefore(date: Date | string): Promise<T[]>\n findCreatedBetween(startDate: Date | string, endDate: Date | string): Promise<T[]>\n findUpdatedAfter(date: Date | string): Promise<T[]>\n findRecentlyUpdated(limit?: number): Promise<T[]>\n findRecentlyCreated(limit?: number): Promise<T[]>\n createWithoutTimestamps(input: unknown): Promise<T>\n updateWithoutTimestamp(id: number, input: unknown): Promise<T>\n touch(id: number): Promise<void>\n getTimestampColumns(): { createdAt: string; updatedAt: string }\n createMany(inputs: unknown[]): Promise<T[]>\n updateMany(ids: (number | string)[], input: unknown): Promise<T[]>\n touchMany(ids: (number | string)[]): Promise<void>\n}\n\n/**\n * Options for the timestamps plugin\n */\nexport interface TimestampsOptions {\n /**\n * Name of the created_at column\n * @default 'created_at'\n */\n createdAtColumn?: string\n\n /**\n * Name of the updated_at column\n * @default 'updated_at'\n */\n updatedAtColumn?: string\n\n /**\n * Whether to set updated_at on insert\n * @default false\n */\n setUpdatedAtOnInsert?: boolean\n\n /**\n * List of tables that should have timestamps\n * If not specified, all tables will have timestamps\n */\n tables?: string[]\n\n /**\n * Tables that should be excluded from timestamps\n */\n excludeTables?: string[]\n\n /**\n * Custom timestamp function (defaults to new Date().toISOString())\n */\n getTimestamp?: () => Date | string | number\n\n /**\n * Date format for database (ISO string by default)\n */\n dateFormat?: 'iso' | 'unix' | 'date'\n\n /**\n * Name of the primary key column used by touch() method\n * @default 'id'\n */\n primaryKeyColumn?: string\n\n /**\n * Logger for plugin operations.\n * Uses KyseraLogger interface from @kysera/core.\n *\n * @default silentLogger (no output)\n */\n logger?: KyseraLogger\n}\n\n/**\n * Zod schema for TimestampsOptions\n * Used for validation and configuration in the kysera-cli\n */\nexport const TimestampsOptionsSchema = z.object({\n createdAtColumn: z.string().optional(),\n updatedAtColumn: z.string().optional(),\n setUpdatedAtOnInsert: z.boolean().optional(),\n tables: z.array(z.string()).optional(),\n excludeTables: z.array(z.string()).optional(),\n getTimestamp: z.function().optional(),\n dateFormat: z.enum(['iso', 'unix', 'date']).optional(),\n primaryKeyColumn: z.string().optional()\n})\n\n/**\n * Repository extended with timestamp methods\n */\nexport type TimestampsRepository<Entity, DB> = Repository<Entity, DB> & TimestampMethods<Entity>\n\n/**\n * Get the current timestamp based on options\n */\nfunction getTimestamp(options: TimestampsOptions): Date | string | number {\n if (options.getTimestamp) {\n return options.getTimestamp()\n }\n\n const now = new Date()\n\n switch (options.dateFormat) {\n case 'unix':\n return Math.floor(now.getTime() / 1000)\n case 'date':\n return now\n case 'iso':\n default:\n return now.toISOString()\n }\n}\n\n/**\n * Check if a table should have timestamps\n */\nfunction shouldApplyTimestamps(tableName: string, options: TimestampsOptions): boolean {\n if (options.excludeTables?.includes(tableName)) {\n return false\n }\n\n if (options.tables) {\n return options.tables.includes(tableName)\n }\n\n return true\n}\n\n/**\n * Type-safe query builder for timestamp operations\n */\nfunction createTimestampQuery(\n executor: Kysely<Record<string, TimestampedTable>>,\n tableName: string,\n column: string\n): {\n select(): SelectQueryBuilder<Record<string, TimestampedTable>, typeof tableName, {}>\n where<V>(\n operator: string,\n value: V\n ): SelectQueryBuilder<Record<string, TimestampedTable>, typeof tableName, {}>\n} {\n return {\n select() {\n return executor.selectFrom(tableName as never)\n },\n where<V>(operator: string, value: V) {\n return executor\n .selectFrom(tableName as never)\n .where(column as never, operator as never, value as never)\n }\n }\n}\n\n/**\n * Check if dialect supports RETURNING clause\n *\n * Database compatibility:\n * - PostgreSQL: Full RETURNING support ✅\n * - SQLite: RETURNING supported in 3.35+ ✅ (most modern versions)\n * - MySQL: No RETURNING support ❌\n * - MSSQL: Uses OUTPUT clause ❌ (different syntax, not compatible with returningAll())\n *\n * **Implementation Notes:**\n * - We only check for MySQL since it's the only major dialect without RETURNING\n * - SQLite 3.35+ is widely deployed (released 2021-03-12)\n * - MSSQL support is minimal in Kysera ecosystem, treated as unsupported here\n * - If MSSQL full support is needed, this function should be extended to detect MSSQL\n *\n * @param executor - Kysely database executor\n * @returns true if RETURNING clause is supported\n */\nfunction supportsReturning<DB>(executor: Kysely<DB>): boolean {\n const dialect = detectDialect(executor)\n // Return false for MySQL (doesn't support RETURNING)\n // Return false for MSSQL (uses OUTPUT, not RETURNING)\n // Return true for PostgreSQL and SQLite\n return dialect !== 'mysql' && dialect !== 'mssql'\n}\n\n/**\n * Timestamps Plugin\n *\n * Automatically manages created_at and updated_at timestamps for database records.\n * Works by overriding repository methods to add timestamp values.\n *\n * ## Features\n *\n * - Automatic `created_at` on insert\n * - Automatic `updated_at` on every update\n * - Configurable column names\n * - Configurable timestamp format (ISO, Unix, Date)\n * - Query helpers: findCreatedAfter, findUpdatedAfter, etc.\n * - Bulk operations: createMany, updateMany, touchMany\n * - **Cross-database support**: Works with PostgreSQL, MySQL, SQLite, and MSSQL\n *\n * ## Transaction Behavior\n *\n * **IMPORTANT**: Timestamp operations respect ACID properties and work correctly with transactions:\n *\n * - ✅ **Commits with transaction**: Timestamps are set using the same executor as the\n * repository operation, so they commit together\n * - ✅ **Rolls back with transaction**: If a transaction is rolled back, all timestamp\n * changes are also rolled back\n * - ✅ **Consistent timestamps**: All operations within a transaction can use the same\n * timestamp by providing a custom `getTimestamp` function\n *\n * ### Correct Transaction Usage\n *\n * ```typescript\n * // ✅ CORRECT: Timestamps are part of transaction\n * await db.transaction().execute(async (trx) => {\n * const repos = createRepositories(trx) // Use transaction executor\n * await repos.users.create({ email: 'test@example.com' }) // created_at auto-set\n * await repos.posts.createMany([...]) // All created_at set consistently\n * // If transaction rolls back, all changes including timestamps roll back\n * })\n * ```\n *\n * ### Consistent Timestamps Across Operations\n *\n * ```typescript\n * // Use shared timestamp for all operations in a transaction\n * const now = new Date()\n * const timestampsWithFixedTime = timestampsPlugin({\n * getTimestamp: () => now\n * })\n *\n * await db.transaction().execute(async (trx) => {\n * // All operations will have the exact same timestamp\n * await repos.users.create(...) // created_at = now\n * await repos.posts.update(...) // updated_at = now\n * })\n * ```\n *\n * @example\n * ```typescript\n * import { timestampsPlugin } from '@kysera/timestamps'\n *\n * const plugin = timestampsPlugin({\n * createdAtColumn: 'created_at',\n * updatedAtColumn: 'updated_at',\n * tables: ['users', 'posts', 'comments']\n * })\n *\n * const orm = createORM(db, [plugin])\n * ```\n */\nexport const timestampsPlugin = (options: TimestampsOptions = {}): Plugin => {\n const {\n createdAtColumn = 'created_at',\n updatedAtColumn = 'updated_at',\n setUpdatedAtOnInsert = false,\n primaryKeyColumn = 'id',\n logger = silentLogger\n } = options\n\n return {\n name: '@kysera/timestamps',\n version: VERSION,\n priority: 50, // Run in the middle, after filtering but before audit\n\n /**\n * Lifecycle: No initialization needed for timestamps plugin\n */\n onInit() {\n // No initialization required\n },\n\n /**\n * Lifecycle: Cleanup resources when executor is destroyed\n */\n async onDestroy() {\n // No cleanup required - timestamps plugin has no persistent resources\n logger.debug('Timestamps plugin destroyed')\n },\n\n extendRepository<T extends object>(repo: T): T {\n // Check if it's actually a repository (has required properties)\n if (!('tableName' in repo) || !('executor' in repo)) {\n return repo\n }\n\n // Type assertion is safe here as we've checked for properties\n const baseRepo = repo as T & {\n tableName: string\n executor: unknown\n create: Function\n update: Function\n }\n\n // Skip if table doesn't support timestamps\n if (!shouldApplyTimestamps(baseRepo.tableName, options)) {\n logger.debug(`Table ${baseRepo.tableName} excluded from timestamps, skipping extension`)\n return repo\n }\n\n logger.debug(`Extending repository for table ${baseRepo.tableName} with timestamp methods`)\n\n // Save original methods\n const originalCreate = baseRepo.create.bind(baseRepo)\n const originalUpdate = baseRepo.update.bind(baseRepo)\n const executor = baseRepo.executor as Kysely<Record<string, TimestampedTable>>\n\n const extendedRepo = {\n ...baseRepo,\n\n // Override create to add timestamps\n async create(input: unknown): Promise<unknown> {\n const data = input as Record<string, unknown>\n const timestamp = getTimestamp(options)\n const dataWithTimestamps: Record<string, unknown> = {\n ...data,\n [createdAtColumn]: data[createdAtColumn] ?? timestamp\n }\n\n if (setUpdatedAtOnInsert) {\n dataWithTimestamps[updatedAtColumn] = data[updatedAtColumn] ?? timestamp\n }\n\n logger.debug(`Creating record in ${baseRepo.tableName} with timestamp ${timestamp}`)\n return await originalCreate(dataWithTimestamps)\n },\n\n // Override update to set updated_at\n async update(id: number, input: unknown): Promise<unknown> {\n const data = input as Record<string, unknown>\n const timestamp = getTimestamp(options)\n const dataWithTimestamp: Record<string, unknown> = {\n ...data,\n [updatedAtColumn]: data[updatedAtColumn] ?? timestamp\n }\n\n logger.debug(`Updating record ${id} in ${baseRepo.tableName} with timestamp ${timestamp}`)\n return await originalUpdate(id, dataWithTimestamp)\n },\n\n /**\n * Find records created after a specific date\n */\n async findCreatedAfter(date: Date | string | number): Promise<unknown[]> {\n const query = createTimestampQuery(executor, baseRepo.tableName, createdAtColumn)\n const result = await query.where('>', String(date)).selectAll().execute()\n return result\n },\n\n /**\n * Find records created before a specific date\n */\n async findCreatedBefore(date: Date | string | number): Promise<unknown[]> {\n const query = createTimestampQuery(executor, baseRepo.tableName, createdAtColumn)\n const result = await query.where('<', String(date)).selectAll().execute()\n return result\n },\n\n /**\n * Find records created between two dates\n */\n async findCreatedBetween(\n startDate: Date | string | number,\n endDate: Date | string | number\n ): Promise<unknown[]> {\n const result = await executor\n .selectFrom(baseRepo.tableName as never)\n .selectAll()\n .where(createdAtColumn as never, '>=', startDate as never)\n .where(createdAtColumn as never, '<=', endDate as never)\n .execute()\n return result\n },\n\n /**\n * Find records updated after a specific date\n */\n async findUpdatedAfter(date: Date | string | number): Promise<unknown[]> {\n const query = createTimestampQuery(executor, baseRepo.tableName, updatedAtColumn)\n const result = await query.where('>', String(date)).selectAll().execute()\n return result\n },\n\n /**\n * Find recently updated records\n */\n async findRecentlyUpdated(limit = 10): Promise<unknown[]> {\n const result = await executor\n .selectFrom(baseRepo.tableName as never)\n .selectAll()\n .orderBy(updatedAtColumn as never, 'desc')\n .limit(limit)\n .execute()\n return result\n },\n\n /**\n * Find recently created records\n */\n async findRecentlyCreated(limit = 10): Promise<unknown[]> {\n const result = await executor\n .selectFrom(baseRepo.tableName as never)\n .selectAll()\n .orderBy(createdAtColumn as never, 'desc')\n .limit(limit)\n .execute()\n return result\n },\n\n /**\n * Create without adding timestamps\n */\n async createWithoutTimestamps(input: unknown): Promise<unknown> {\n logger.debug(`Creating record in ${baseRepo.tableName} without timestamps`)\n return await originalCreate(input)\n },\n\n /**\n * Update without modifying timestamp\n */\n async updateWithoutTimestamp(id: number, input: unknown): Promise<unknown> {\n logger.debug(`Updating record ${id} in ${baseRepo.tableName} without timestamp`)\n return await originalUpdate(id, input)\n },\n\n /**\n * Touch a record (update its timestamp)\n */\n async touch(id: number): Promise<void> {\n const timestamp = getTimestamp(options)\n const updateData = { [updatedAtColumn]: timestamp }\n\n logger.info(`Touching record ${id} in ${baseRepo.tableName}`)\n await executor\n .updateTable(baseRepo.tableName as never)\n .set(updateData as never)\n .where(primaryKeyColumn as never, '=', id as never)\n .execute()\n },\n\n /**\n * Get the timestamp column names\n */\n getTimestampColumns(): { createdAt: string; updatedAt: string } {\n return {\n createdAt: createdAtColumn,\n updatedAt: updatedAtColumn\n }\n },\n\n /**\n * Create multiple records with timestamps\n * Uses efficient bulk INSERT with automatic timestamp injection.\n * Supports PostgreSQL, MySQL, SQLite, and MSSQL with appropriate fallbacks.\n */\n async createMany(inputs: unknown[]): Promise<unknown[]> {\n // Handle empty arrays gracefully\n if (!inputs || inputs.length === 0) {\n return []\n }\n\n const timestamp = getTimestamp(options)\n const dataWithTimestamps = inputs.map(input => {\n const data = input as Record<string, unknown>\n const result: Record<string, unknown> = {\n ...data,\n [createdAtColumn]: data[createdAtColumn] ?? timestamp\n }\n\n if (setUpdatedAtOnInsert) {\n result[updatedAtColumn] = data[updatedAtColumn] ?? timestamp\n }\n\n return result\n })\n\n logger.info(\n `Creating ${inputs.length} records in ${baseRepo.tableName} with timestamp ${timestamp}`\n )\n\n // Check if dialect supports RETURNING\n if (supportsReturning(executor)) {\n // Use RETURNING for PostgreSQL/SQLite - most efficient\n const result = await executor\n .insertInto(baseRepo.tableName as never)\n .values(dataWithTimestamps as never)\n .returningAll()\n .execute()\n\n return result\n } else {\n // Fallback for MySQL/MSSQL - insert then select\n // This approach works for auto-increment primary keys\n await executor\n .insertInto(baseRepo.tableName as never)\n .values(dataWithTimestamps as never)\n .execute()\n\n // Fetch inserted records by matching on unique columns\n // For simplicity, we fetch the most recently created records\n // This works well when created_at is set to the same timestamp\n const insertedCount = dataWithTimestamps.length\n const result = await executor\n .selectFrom(baseRepo.tableName as never)\n .selectAll()\n .where(createdAtColumn as never, '=', timestamp as never)\n .orderBy(primaryKeyColumn as never, 'desc')\n .limit(insertedCount)\n .execute()\n\n // Reverse to maintain insertion order\n return result.reverse()\n }\n },\n\n /**\n * Update multiple records (sets updated_at for all)\n * Updates all specified records with the same data\n */\n async updateMany(ids: (number | string)[], input: unknown): Promise<unknown[]> {\n // Handle empty arrays gracefully\n if (!ids || ids.length === 0) {\n return []\n }\n\n const data = input as Record<string, unknown>\n const timestamp = getTimestamp(options)\n const dataWithTimestamp: Record<string, unknown> = {\n ...data,\n [updatedAtColumn]: data[updatedAtColumn] ?? timestamp\n }\n\n logger.info(\n `Updating ${ids.length} records in ${baseRepo.tableName} with timestamp ${timestamp}`\n )\n\n // Use Kysely's update with IN clause for efficient bulk update\n await executor\n .updateTable(baseRepo.tableName as never)\n .set(dataWithTimestamp as never)\n .where(primaryKeyColumn as never, 'in', ids as never)\n .execute()\n\n // Fetch and return updated records\n const result = await executor\n .selectFrom(baseRepo.tableName as never)\n .selectAll()\n .where(primaryKeyColumn as never, 'in', ids as never)\n .execute()\n\n return result\n },\n\n /**\n * Touch multiple records (update updated_at only)\n * Efficiently updates only the timestamp column\n */\n async touchMany(ids: (number | string)[]): Promise<void> {\n // Handle empty arrays gracefully\n if (!ids || ids.length === 0) {\n return\n }\n\n const timestamp = getTimestamp(options)\n const updateData = { [updatedAtColumn]: timestamp }\n\n logger.info(`Touching ${ids.length} records in ${baseRepo.tableName}`)\n await executor\n .updateTable(baseRepo.tableName as never)\n .set(updateData as never)\n .where(primaryKeyColumn as never, 'in', ids as never)\n .execute()\n }\n }\n\n return extendedRepo as T\n }\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kysera/timestamps",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "Automatic timestamp management plugin for Kysely repositories",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -27,24 +27,25 @@
|
|
|
27
27
|
"author": "Kysera Team",
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@kysera/core": "0.7.
|
|
30
|
+
"@kysera/core": "0.7.4"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/better-sqlite3": "^7.6.13",
|
|
34
34
|
"@types/node": "^24.10.1",
|
|
35
|
-
"@vitest/coverage-v8": "^4.0.
|
|
35
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
36
36
|
"better-sqlite3": "^12.5.0",
|
|
37
37
|
"kysely": "^0.28.9",
|
|
38
38
|
"tsup": "^8.5.1",
|
|
39
39
|
"typescript": "^5.9.3",
|
|
40
|
-
"vitest": "^4.0.
|
|
40
|
+
"vitest": "^4.0.16",
|
|
41
41
|
"zod": "^4.1.13",
|
|
42
|
-
"@kysera/repository": "0.7.
|
|
42
|
+
"@kysera/repository": "0.7.4"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"kysely": ">=0.28.8",
|
|
46
46
|
"zod": "^4.1.13",
|
|
47
|
-
"@kysera/repository": "0.7.
|
|
47
|
+
"@kysera/repository": "0.7.4",
|
|
48
|
+
"@kysera/executor": "0.7.4"
|
|
48
49
|
},
|
|
49
50
|
"peerDependenciesMeta": {
|
|
50
51
|
"zod": {
|
|
@@ -63,6 +64,6 @@
|
|
|
63
64
|
"test:watch": "vitest",
|
|
64
65
|
"test:coverage": "vitest run --coverage",
|
|
65
66
|
"typecheck": "tsc --noEmit",
|
|
66
|
-
"lint": "eslint src
|
|
67
|
+
"lint": "eslint src"
|
|
67
68
|
}
|
|
68
69
|
}
|