@@ -25,13 +25,17 @@ export type SnapshotProjectionConfig<
2525 tableName : TTable ;
2626
2727 /**
28- * The primary key columns that uniquely identify a row
28+ * @deprecated The primary key columns are now automatically inferred from the keys returned by extractKeys.
29+ * This field is optional and will be removed in a future version.
30+ *
31+ * If provided, it will be validated against the keys returned by extractKeys.
2932 * e.g., ['tenant_id', 'cart_id', 'partition']
3033 */
31- primaryKeys : string [ ] ;
34+ primaryKeys ? : string [ ] ;
3235
3336 /**
34- * Extract primary key values from the event data
37+ * Extract primary key values from the event data.
38+ * The keys of the returned object will be used as the primary key columns for upsert operations.
3539 */
3640 extractKeys : (
3741 event : ProjectionEvent < E > ,
@@ -78,7 +82,6 @@ export type SnapshotProjectionConfig<
7882 * ```typescript
7983 * const cartProjection = createSnapshotProjection({
8084 * tableName: 'carts',
81- * primaryKeys: ['tenant_id', 'cart_id', 'partition'],
8285 * extractKeys: (event, partition) => ({
8386 * tenant_id: event.data.eventMeta.tenantId,
8487 * cart_id: event.data.eventMeta.cartId,
@@ -103,21 +106,24 @@ export function createSnapshotProjection<
103106> (
104107 config : SnapshotProjectionConfig < TState , TTable , E > ,
105108) : ProjectionHandler < DatabaseExecutor , E > {
106- const {
107- tableName,
108- primaryKeys,
109- extractKeys,
110- evolve,
111- initialState,
112- mapToColumns,
113- } = config ;
109+ const { tableName, extractKeys, evolve, initialState, mapToColumns } = config ;
110+
111+ // Cache the inferred primary keys after the first call
112+ let inferredPrimaryKeys : string [ ] | undefined ;
114113
115114 return async (
116115 { db, partition } : ProjectionContext < DatabaseExecutor > ,
117116 event : ProjectionEvent < E > ,
118117 ) => {
119118 const keys = extractKeys ( event , partition ) ;
120119
120+ // Infer primary keys from extractKeys on first call
121+ if ( ! inferredPrimaryKeys ) {
122+ inferredPrimaryKeys = Object . keys ( keys ) ;
123+ }
124+
125+ const primaryKeys = inferredPrimaryKeys ;
126+
121127 // Check if event is newer than what we've already processed
122128 // Note: Casting to `any` is necessary because Kysely cannot infer types for dynamic table names.
123129 // The table name is provided at runtime, so TypeScript cannot verify the table structure at compile time.
@@ -212,7 +218,6 @@ export function createSnapshotProjection<
212218 * ['CartCreated', 'ItemAddedToCart', 'ItemRemovedFromCart'],
213219 * {
214220 * tableName: 'carts',
215- * primaryKeys: ['tenant_id', 'cart_id', 'partition'],
216221 * extractKeys: (event, partition) => ({
217222 * tenant_id: event.data.eventMeta.tenantId,
218223 * cart_id: event.data.eventMeta.cartId,
0 commit comments