@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 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
  [![Version](https://img.shields.io/npm/v/@kysera/timestamps.svg)](https://www.npmjs.com/package/@kysera/timestamps)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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 with @kysera/repository
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({ /* config */ })
81
+ pool: new Pool({
82
+ /* config */
83
+ })
81
84
  })
82
85
  })
83
86
 
84
- // Create plugin container with timestamps plugin
85
- const orm = await createORM(db, [
86
- timestampsPlugin() // ✨ That's it!
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 repository
90
- const userRepo = orm.createRepository((executor) => {
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: (row) => row as User,
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) // ✅ 2024-01-15T10:30:00.000Z
111
- console.log(user.updated_at) // null (only set on update)
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) // ✅ 2024-01-15T10:35:00.000Z
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, // All tables
176
+ tables: undefined, // All tables
171
177
  excludeTables: undefined, // No exclusions
172
- primaryKeyColumn: 'id' // Default primary key
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> // ✅ Custom name
199
- modified: Date | null // ✅ Custom name
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) // 2024-01-15T10:30:00.000Z
321
- console.log(user.updated_at) // 2024-01-15T10:30:00.000Z (same!)
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) // 2024-01-15T10:35:00.000Z (different)
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> // Custom primary key
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) // ✅ 2024-01-15T10:30:00.000Z
382
- console.log(user.updated_at) // null (default behavior)
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' // ✅ Uses this instead
402
+ created_at: '2020-01-01T00:00:00.000Z' // ✅ Uses this instead
393
403
  })
394
404
 
395
- console.log(user.created_at) // 2020-01-01T00:00:00.000Z
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) // ✅ 2024-01-15T10:35:00.000Z
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' // ✅ Uses this instead
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) // null (not set)
437
- console.log(user.updated_at) // null (not set)
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) // null (unchanged)
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]) // Most recent user
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) // Same timestamp for all
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) // 0
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) // 'active'
650
- console.log(user.updated_at) // Same timestamp for all
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) // 0
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) // All have same new timestamp
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([]) // No-op
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
- const orm = await createORM(db, [
842
+ // Register all plugins with Unified Execution Layer
843
+ const executor = await createExecutor(db, [
832
844
  timestampsPlugin(),
833
845
  softDeletePlugin(),
834
- auditPlugin({ userId: currentUserId })
846
+ auditPlugin({ getUserId: () => currentUserId })
835
847
  ])
836
848
 
837
- // All plugins work together:
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 (trx) => {
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: (row) => ({
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), // Convert to Date
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) // Error: number not assignable
1052
+ await userRepo.findCreatedAfter(12345) // Error: number not assignable
1039
1053
 
1040
1054
  // ❌ Type error: method doesn't exist
1041
- await userRepo.nonExistentMethod() // Error: method doesn't exist
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> // Auto-generated
1055
- updated_at: Date | null // Nullable
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', // ✅ Must match schema
1062
- updatedAtColumn: 'updated_at' // ✅ Must match schema
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 // Default: 'created_at'
1079
- updatedAtColumn?: string // Default: 'updated_at'
1080
- setUpdatedAtOnInsert?: boolean // Default: false
1081
- tables?: string[] // Default: undefined (all tables)
1082
- excludeTables?: string[] // Default: undefined
1083
- getTimestamp?: () => Date | string | number // Custom generator
1084
- dateFormat?: 'iso' | 'unix' | 'date' // Default: 'iso'
1085
- primaryKeyColumn?: string // Default: 'id'
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 // ✅ Null until first update
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 // ❌ What value on insert?
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> // ✅ Auto-generated
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 // Must be provided or plugin adds it
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' // Portable across databases
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' // Good for INTEGER columns
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) // Only updates timestamp
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) // Fetches + updates all fields
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 | Base | With Timestamps | Overhead |
1417
- |-----------|------|----------------|----------|
1418
- | **create** | 2ms | 2.05ms | +0.05ms |
1419
- | **update** | 2ms | 2.05ms | +0.05ms |
1420
- | **findById** | 1ms | 1ms | 0ms |
1421
- | **findRecentlyCreated** | - | 5ms | N/A |
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 | Generation Time | Storage Size | Query Performance |
1426
- |--------|----------------|--------------|-------------------|
1427
- | **ISO** | ~0.001ms | 24-27 bytes | Medium |
1428
- | **Unix** | ~0.0005ms | 4-8 bytes | Fast |
1429
- | **Date** | ~0.001ms | Varies | Medium |
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'] // ❌ Users excluded!
1515
+ excludeTables: ['users'] // ❌ Users excluded!
1484
1516
  })
1485
1517
 
1486
1518
  // Fix: Remove from exclusions
1487
1519
  const plugin = timestampsPlugin({
1488
- excludeTables: [] // ✅ No exclusions
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' // ✅ ISO string
1543
+ dateFormat: 'iso' // ✅ ISO string
1511
1544
  })
1512
1545
 
1513
1546
  // For INTEGER columns
1514
1547
  const plugin = timestampsPlugin({
1515
- dateFormat: 'unix' // ✅ Unix timestamp
1548
+ dateFormat: 'unix' // ✅ Unix timestamp
1516
1549
  })
1517
1550
 
1518
1551
  // For DATE columns
1519
1552
  const plugin = timestampsPlugin({
1520
- dateFormat: 'date' // ✅ Date object
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() // ❌ Undefined
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((executor) => {
1571
+ const userRepo = orm.createRepository(executor => {
1539
1572
  const factory = createRepositoryFactory(executor)
1540
1573
  return factory.create(/* ... */)
1541
1574
  })
1542
- await userRepo.findRecentlyCreated() // ✅ Works!
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 (trx) => {
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({ /* ... */ }) // ✅ Timestamps added
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 { Repository, Plugin } from '@kysera/repository';
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 N=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 d(s){if(s.getTimestamp)return s.getTimestamp();let r=new Date;switch(s.dateFormat){case "unix":return Math.floor(r.getTime()/1e3);case "date":return r;case "iso":default:return r.toISOString()}}function k(s,r){return r.excludeTables?.includes(s)?false:r.tables?r.tables.includes(s):true}function w(s,r,i){return {select(){return s.selectFrom(r)},where(g,b){return s.selectFrom(r).where(i,g,b)}}}var R=(s={})=>{let{createdAtColumn:r="created_at",updatedAtColumn:i="updated_at",setUpdatedAtOnInsert:g=false,primaryKeyColumn:b="id",logger:u=silentLogger}=s;return {name:"@kysera/timestamps",version:"0.5.1",interceptQuery(c,t){return c},extendRepository(c){if(!("tableName"in c)||!("executor"in c))return c;let t=c;if(!k(t.tableName,s))return u.debug(`Table ${t.tableName} excluded from timestamps, skipping extension`),c;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 n=e,a=d(s),l={...n,[r]:n[r]??a};return g&&(l[i]=n[i]??a),u.debug(`Creating record in ${t.tableName} with timestamp ${a}`),await T(l)},async update(e,n){let a=n,l=d(s),p={...a,[i]:a[i]??l};return u.debug(`Updating record ${e} in ${t.tableName} with timestamp ${l}`),await f(e,p)},async findCreatedAfter(e){return await w(o,t.tableName,r).where(">",String(e)).selectAll().execute()},async findCreatedBefore(e){return await w(o,t.tableName,r).where("<",String(e)).selectAll().execute()},async findCreatedBetween(e,n){return await o.selectFrom(t.tableName).selectAll().where(r,">=",e).where(r,"<=",n).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(r,"desc").limit(e).execute()},async createWithoutTimestamps(e){return u.debug(`Creating record in ${t.tableName} without timestamps`),await T(e)},async updateWithoutTimestamp(e,n){return u.debug(`Updating record ${e} in ${t.tableName} without timestamp`),await f(e,n)},async touch(e){let n=d(s),a={[i]:n};u.info(`Touching record ${e} in ${t.tableName}`),await o.updateTable(t.tableName).set(a).where(b,"=",e).execute();},getTimestampColumns(){return {createdAt:r,updatedAt:i}},async createMany(e){if(!e||e.length===0)return [];let n=d(s),a=e.map(p=>{let y=p,h={...y,[r]:y[r]??n};return g&&(h[i]=y[i]??n),h});return u.info(`Creating ${e.length} records in ${t.tableName} with timestamp ${n}`),await o.insertInto(t.tableName).values(a).returningAll().execute()},async updateMany(e,n){if(!e||e.length===0)return [];let a=n,l=d(s),p={...a,[i]:a[i]??l};return u.info(`Updating ${e.length} records in ${t.tableName} with timestamp ${l}`),await o.updateTable(t.tableName).set(p).where("id","in",e).execute(),await o.selectFrom(t.tableName).selectAll().where("id","in",e).execute()},async touchMany(e){if(!e||e.length===0)return;let n=d(s),a={[i]:n};u.info(`Touching ${e.length} records in ${t.tableName}`),await o.updateTable(t.tableName).set(a).where("id","in",e).execute();}}}}};export{N as TimestampsOptionsSchema,R as timestampsPlugin};//# sourceMappingURL=index.js.map
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.2",
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.2"
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.15",
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.15",
40
+ "vitest": "^4.0.16",
41
41
  "zod": "^4.1.13",
42
- "@kysera/repository": "0.7.2"
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.2"
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 test --ext .ts"
67
+ "lint": "eslint src"
67
68
  }
68
69
  }