@tanstack/db 0.0.22 → 0.0.23

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.
@@ -1 +1 @@
1
- {"version":3,"file":"transactions.js","sources":["../../src/transactions.ts"],"sourcesContent":["import { createDeferred } from \"./deferred\"\nimport type { Deferred } from \"./deferred\"\nimport type {\n MutationFn,\n OperationType,\n PendingMutation,\n TransactionConfig,\n TransactionState,\n TransactionWithMutations,\n} from \"./types\"\n\nconst transactions: Array<Transaction<any>> = []\nlet transactionStack: Array<Transaction<any>> = []\n\nlet sequenceNumber = 0\n\n/**\n * Creates a new transaction for grouping multiple collection operations\n * @param config - Transaction configuration with mutation function\n * @returns A new Transaction instance\n * @example\n * // Basic transaction usage\n * const tx = createTransaction({\n * mutationFn: async ({ transaction }) => {\n * // Send all mutations to API\n * await api.saveChanges(transaction.mutations)\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * collection.update(\"2\", draft => { draft.completed = true })\n * })\n *\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle transaction errors\n * try {\n * const tx = createTransaction({\n * mutationFn: async () => { throw new Error(\"API failed\") }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"New item\" })\n * })\n *\n * await tx.isPersisted.promise\n * } catch (error) {\n * console.log('Transaction failed:', error)\n * }\n *\n * @example\n * // Manual commit control\n * const tx = createTransaction({\n * autoCommit: false,\n * mutationFn: async () => {\n * // API call\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * // Commit later\n * await tx.commit()\n */\nexport function createTransaction<\n TData extends object = Record<string, unknown>,\n>(config: TransactionConfig<TData>): Transaction<TData> {\n const newTransaction = new Transaction<TData>(config)\n transactions.push(newTransaction)\n return newTransaction\n}\n\n/**\n * Gets the currently active ambient transaction, if any\n * Used internally by collection operations to join existing transactions\n * @returns The active transaction or undefined if none is active\n * @example\n * // Check if operations will join an ambient transaction\n * const ambientTx = getActiveTransaction()\n * if (ambientTx) {\n * console.log('Operations will join transaction:', ambientTx.id)\n * }\n */\nexport function getActiveTransaction(): Transaction | undefined {\n if (transactionStack.length > 0) {\n return transactionStack.slice(-1)[0]\n } else {\n return undefined\n }\n}\n\nfunction registerTransaction(tx: Transaction<any>) {\n transactionStack.push(tx)\n}\n\nfunction unregisterTransaction(tx: Transaction<any>) {\n transactionStack = transactionStack.filter((t) => t.id !== tx.id)\n}\n\nfunction removeFromPendingList(tx: Transaction<any>) {\n const index = transactions.findIndex((t) => t.id === tx.id)\n if (index !== -1) {\n transactions.splice(index, 1)\n }\n}\n\nclass Transaction<\n T extends object = Record<string, unknown>,\n TOperation extends OperationType = OperationType,\n> {\n public id: string\n public state: TransactionState\n public mutationFn: MutationFn<T>\n public mutations: Array<PendingMutation<T, TOperation>>\n public isPersisted: Deferred<Transaction<T, TOperation>>\n public autoCommit: boolean\n public createdAt: Date\n public sequenceNumber: number\n public metadata: Record<string, unknown>\n public error?: {\n message: string\n error: Error\n }\n\n constructor(config: TransactionConfig<T>) {\n if (typeof config.mutationFn === `undefined`) {\n throw `mutationFn is required when creating a transaction`\n }\n this.id = config.id ?? crypto.randomUUID()\n this.mutationFn = config.mutationFn\n this.state = `pending`\n this.mutations = []\n this.isPersisted = createDeferred<Transaction<T, TOperation>>()\n this.autoCommit = config.autoCommit ?? true\n this.createdAt = new Date()\n this.sequenceNumber = sequenceNumber++\n this.metadata = config.metadata ?? {}\n }\n\n setState(newState: TransactionState) {\n this.state = newState\n\n if (newState === `completed` || newState === `failed`) {\n removeFromPendingList(this)\n }\n }\n\n /**\n * Execute collection operations within this transaction\n * @param callback - Function containing collection operations to group together\n * @returns This transaction for chaining\n * @example\n * // Group multiple operations\n * const tx = createTransaction({ mutationFn: async () => {\n * // Send to API\n * }})\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * collection.update(\"2\", draft => { draft.completed = true })\n * collection.delete(\"3\")\n * })\n *\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle mutate errors\n * try {\n * tx.mutate(() => {\n * collection.insert({ id: \"invalid\" }) // This might throw\n * })\n * } catch (error) {\n * console.log('Mutation failed:', error)\n * }\n *\n * @example\n * // Manual commit control\n * const tx = createTransaction({ autoCommit: false, mutationFn: async () => {} })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * // Commit later when ready\n * await tx.commit()\n */\n mutate(callback: () => void): Transaction<T> {\n if (this.state !== `pending`) {\n throw `You can no longer call .mutate() as the transaction is no longer pending`\n }\n\n registerTransaction(this)\n try {\n callback()\n } finally {\n unregisterTransaction(this)\n }\n\n if (this.autoCommit) {\n this.commit()\n }\n\n return this\n }\n\n applyMutations(mutations: Array<PendingMutation<any>>): void {\n for (const newMutation of mutations) {\n const existingIndex = this.mutations.findIndex(\n (m) => m.globalKey === newMutation.globalKey\n )\n\n if (existingIndex >= 0) {\n // Replace existing mutation\n this.mutations[existingIndex] = newMutation\n } else {\n // Insert new mutation\n this.mutations.push(newMutation)\n }\n }\n }\n\n /**\n * Rollback the transaction and any conflicting transactions\n * @param config - Configuration for rollback behavior\n * @returns This transaction for chaining\n * @example\n * // Manual rollback\n * const tx = createTransaction({ mutationFn: async () => {\n * // Send to API\n * }})\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * })\n *\n * // Rollback if needed\n * if (shouldCancel) {\n * tx.rollback()\n * }\n *\n * @example\n * // Handle rollback cascade (automatic)\n * const tx1 = createTransaction({ mutationFn: async () => {} })\n * const tx2 = createTransaction({ mutationFn: async () => {} })\n *\n * tx1.mutate(() => collection.update(\"1\", draft => { draft.value = \"A\" }))\n * tx2.mutate(() => collection.update(\"1\", draft => { draft.value = \"B\" })) // Same item\n *\n * tx1.rollback() // This will also rollback tx2 due to conflict\n *\n * @example\n * // Handle rollback in error scenarios\n * try {\n * await tx.isPersisted.promise\n * } catch (error) {\n * console.log('Transaction was rolled back:', error)\n * // Transaction automatically rolled back on mutation function failure\n * }\n */\n rollback(config?: { isSecondaryRollback?: boolean }): Transaction<T> {\n const isSecondaryRollback = config?.isSecondaryRollback ?? false\n if (this.state === `completed`) {\n throw `You can no longer call .rollback() as the transaction is already completed`\n }\n\n this.setState(`failed`)\n\n // See if there's any other transactions w/ mutations on the same ids\n // and roll them back as well.\n if (!isSecondaryRollback) {\n const mutationIds = new Set()\n this.mutations.forEach((m) => mutationIds.add(m.globalKey))\n for (const t of transactions) {\n t.state === `pending` &&\n t.mutations.some((m) => mutationIds.has(m.globalKey)) &&\n t.rollback({ isSecondaryRollback: true })\n }\n }\n\n // Reject the promise\n this.isPersisted.reject(this.error?.error)\n this.touchCollection()\n\n return this\n }\n\n // Tell collection that something has changed with the transaction\n touchCollection(): void {\n const hasCalled = new Set()\n for (const mutation of this.mutations) {\n if (!hasCalled.has(mutation.collection.id)) {\n mutation.collection.onTransactionStateChange()\n\n // Only call commitPendingTransactions if there are pending sync transactions\n if (mutation.collection.pendingSyncedTransactions.length > 0) {\n mutation.collection.commitPendingTransactions()\n }\n\n hasCalled.add(mutation.collection.id)\n }\n }\n }\n\n /**\n * Commit the transaction and execute the mutation function\n * @returns Promise that resolves to this transaction when complete\n * @example\n * // Manual commit (when autoCommit is false)\n * const tx = createTransaction({\n * autoCommit: false,\n * mutationFn: async ({ transaction }) => {\n * await api.saveChanges(transaction.mutations)\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * })\n *\n * await tx.commit() // Manually commit\n *\n * @example\n * // Handle commit errors\n * try {\n * const tx = createTransaction({\n * mutationFn: async () => { throw new Error(\"API failed\") }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * await tx.commit()\n * } catch (error) {\n * console.log('Commit failed, transaction rolled back:', error)\n * }\n *\n * @example\n * // Check transaction state after commit\n * await tx.commit()\n * console.log(tx.state) // \"completed\" or \"failed\"\n */\n async commit(): Promise<Transaction<T>> {\n if (this.state !== `pending`) {\n throw `You can no longer call .commit() as the transaction is no longer pending`\n }\n\n this.setState(`persisting`)\n\n if (this.mutations.length === 0) {\n this.setState(`completed`)\n\n return this\n }\n\n // Run mutationFn\n try {\n // At this point we know there's at least one mutation\n // We've already verified mutations is non-empty, so this cast is safe\n // Use a direct type assertion instead of object spreading to preserve the original type\n await this.mutationFn({\n transaction: this as unknown as TransactionWithMutations<T>,\n })\n\n this.setState(`completed`)\n this.touchCollection()\n\n this.isPersisted.resolve(this)\n } catch (error) {\n // Update transaction with error information\n this.error = {\n message: error instanceof Error ? error.message : String(error),\n error: error instanceof Error ? error : new Error(String(error)),\n }\n\n // rollback the transaction\n return this.rollback()\n }\n\n return this\n }\n\n /**\n * Compare two transactions by their createdAt time and sequence number in order\n * to sort them in the order they were created.\n * @param other - The other transaction to compare to\n * @returns -1 if this transaction was created before the other, 1 if it was created after, 0 if they were created at the same time\n */\n compareCreatedAt(other: Transaction<any>): number {\n const createdAtComparison =\n this.createdAt.getTime() - other.createdAt.getTime()\n if (createdAtComparison !== 0) {\n return createdAtComparison\n }\n return this.sequenceNumber - other.sequenceNumber\n }\n}\n\nexport type { Transaction }\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"transactions.js","sources":["../../src/transactions.ts"],"sourcesContent":["import { createDeferred } from \"./deferred\"\nimport type { Deferred } from \"./deferred\"\nimport type {\n MutationFn,\n PendingMutation,\n TransactionConfig,\n TransactionState,\n TransactionWithMutations,\n} from \"./types\"\n\nconst transactions: Array<Transaction<any>> = []\nlet transactionStack: Array<Transaction<any>> = []\n\nlet sequenceNumber = 0\n\n/**\n * Creates a new transaction for grouping multiple collection operations\n * @param config - Transaction configuration with mutation function\n * @returns A new Transaction instance\n * @example\n * // Basic transaction usage\n * const tx = createTransaction({\n * mutationFn: async ({ transaction }) => {\n * // Send all mutations to API\n * await api.saveChanges(transaction.mutations)\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * collection.update(\"2\", draft => { draft.completed = true })\n * })\n *\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle transaction errors\n * try {\n * const tx = createTransaction({\n * mutationFn: async () => { throw new Error(\"API failed\") }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"New item\" })\n * })\n *\n * await tx.isPersisted.promise\n * } catch (error) {\n * console.log('Transaction failed:', error)\n * }\n *\n * @example\n * // Manual commit control\n * const tx = createTransaction({\n * autoCommit: false,\n * mutationFn: async () => {\n * // API call\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * // Commit later\n * await tx.commit()\n */\nexport function createTransaction<T extends object = Record<string, unknown>>(\n config: TransactionConfig<T>\n): Transaction<T> {\n const newTransaction = new Transaction<T>(config)\n transactions.push(newTransaction)\n return newTransaction\n}\n\n/**\n * Gets the currently active ambient transaction, if any\n * Used internally by collection operations to join existing transactions\n * @returns The active transaction or undefined if none is active\n * @example\n * // Check if operations will join an ambient transaction\n * const ambientTx = getActiveTransaction()\n * if (ambientTx) {\n * console.log('Operations will join transaction:', ambientTx.id)\n * }\n */\nexport function getActiveTransaction(): Transaction | undefined {\n if (transactionStack.length > 0) {\n return transactionStack.slice(-1)[0]\n } else {\n return undefined\n }\n}\n\nfunction registerTransaction(tx: Transaction<any>) {\n transactionStack.push(tx)\n}\n\nfunction unregisterTransaction(tx: Transaction<any>) {\n transactionStack = transactionStack.filter((t) => t.id !== tx.id)\n}\n\nfunction removeFromPendingList(tx: Transaction<any>) {\n const index = transactions.findIndex((t) => t.id === tx.id)\n if (index !== -1) {\n transactions.splice(index, 1)\n }\n}\n\nclass Transaction<T extends object = Record<string, unknown>> {\n public id: string\n public state: TransactionState\n public mutationFn: MutationFn<T>\n public mutations: Array<PendingMutation<T>>\n public isPersisted: Deferred<Transaction<T>>\n public autoCommit: boolean\n public createdAt: Date\n public sequenceNumber: number\n public metadata: Record<string, unknown>\n public error?: {\n message: string\n error: Error\n }\n\n constructor(config: TransactionConfig<T>) {\n if (typeof config.mutationFn === `undefined`) {\n throw `mutationFn is required when creating a transaction`\n }\n this.id = config.id ?? crypto.randomUUID()\n this.mutationFn = config.mutationFn\n this.state = `pending`\n this.mutations = []\n this.isPersisted = createDeferred<Transaction<T>>()\n this.autoCommit = config.autoCommit ?? true\n this.createdAt = new Date()\n this.sequenceNumber = sequenceNumber++\n this.metadata = config.metadata ?? {}\n }\n\n setState(newState: TransactionState) {\n this.state = newState\n\n if (newState === `completed` || newState === `failed`) {\n removeFromPendingList(this)\n }\n }\n\n /**\n * Execute collection operations within this transaction\n * @param callback - Function containing collection operations to group together\n * @returns This transaction for chaining\n * @example\n * // Group multiple operations\n * const tx = createTransaction({ mutationFn: async () => {\n * // Send to API\n * }})\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * collection.update(\"2\", draft => { draft.completed = true })\n * collection.delete(\"3\")\n * })\n *\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle mutate errors\n * try {\n * tx.mutate(() => {\n * collection.insert({ id: \"invalid\" }) // This might throw\n * })\n * } catch (error) {\n * console.log('Mutation failed:', error)\n * }\n *\n * @example\n * // Manual commit control\n * const tx = createTransaction({ autoCommit: false, mutationFn: async () => {} })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * // Commit later when ready\n * await tx.commit()\n */\n mutate(callback: () => void): Transaction<T> {\n if (this.state !== `pending`) {\n throw `You can no longer call .mutate() as the transaction is no longer pending`\n }\n\n registerTransaction(this)\n try {\n callback()\n } finally {\n unregisterTransaction(this)\n }\n\n if (this.autoCommit) {\n this.commit()\n }\n\n return this\n }\n\n applyMutations(mutations: Array<PendingMutation<any>>): void {\n for (const newMutation of mutations) {\n const existingIndex = this.mutations.findIndex(\n (m) => m.globalKey === newMutation.globalKey\n )\n\n if (existingIndex >= 0) {\n // Replace existing mutation\n this.mutations[existingIndex] = newMutation\n } else {\n // Insert new mutation\n this.mutations.push(newMutation)\n }\n }\n }\n\n /**\n * Rollback the transaction and any conflicting transactions\n * @param config - Configuration for rollback behavior\n * @returns This transaction for chaining\n * @example\n * // Manual rollback\n * const tx = createTransaction({ mutationFn: async () => {\n * // Send to API\n * }})\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * })\n *\n * // Rollback if needed\n * if (shouldCancel) {\n * tx.rollback()\n * }\n *\n * @example\n * // Handle rollback cascade (automatic)\n * const tx1 = createTransaction({ mutationFn: async () => {} })\n * const tx2 = createTransaction({ mutationFn: async () => {} })\n *\n * tx1.mutate(() => collection.update(\"1\", draft => { draft.value = \"A\" }))\n * tx2.mutate(() => collection.update(\"1\", draft => { draft.value = \"B\" })) // Same item\n *\n * tx1.rollback() // This will also rollback tx2 due to conflict\n *\n * @example\n * // Handle rollback in error scenarios\n * try {\n * await tx.isPersisted.promise\n * } catch (error) {\n * console.log('Transaction was rolled back:', error)\n * // Transaction automatically rolled back on mutation function failure\n * }\n */\n rollback(config?: { isSecondaryRollback?: boolean }): Transaction<T> {\n const isSecondaryRollback = config?.isSecondaryRollback ?? false\n if (this.state === `completed`) {\n throw `You can no longer call .rollback() as the transaction is already completed`\n }\n\n this.setState(`failed`)\n\n // See if there's any other transactions w/ mutations on the same ids\n // and roll them back as well.\n if (!isSecondaryRollback) {\n const mutationIds = new Set()\n this.mutations.forEach((m) => mutationIds.add(m.globalKey))\n for (const t of transactions) {\n t.state === `pending` &&\n t.mutations.some((m) => mutationIds.has(m.globalKey)) &&\n t.rollback({ isSecondaryRollback: true })\n }\n }\n\n // Reject the promise\n this.isPersisted.reject(this.error?.error)\n this.touchCollection()\n\n return this\n }\n\n // Tell collection that something has changed with the transaction\n touchCollection(): void {\n const hasCalled = new Set()\n for (const mutation of this.mutations) {\n if (!hasCalled.has(mutation.collection.id)) {\n mutation.collection.onTransactionStateChange()\n\n // Only call commitPendingTransactions if there are pending sync transactions\n if (mutation.collection.pendingSyncedTransactions.length > 0) {\n mutation.collection.commitPendingTransactions()\n }\n\n hasCalled.add(mutation.collection.id)\n }\n }\n }\n\n /**\n * Commit the transaction and execute the mutation function\n * @returns Promise that resolves to this transaction when complete\n * @example\n * // Manual commit (when autoCommit is false)\n * const tx = createTransaction({\n * autoCommit: false,\n * mutationFn: async ({ transaction }) => {\n * await api.saveChanges(transaction.mutations)\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * })\n *\n * await tx.commit() // Manually commit\n *\n * @example\n * // Handle commit errors\n * try {\n * const tx = createTransaction({\n * mutationFn: async () => { throw new Error(\"API failed\") }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * await tx.commit()\n * } catch (error) {\n * console.log('Commit failed, transaction rolled back:', error)\n * }\n *\n * @example\n * // Check transaction state after commit\n * await tx.commit()\n * console.log(tx.state) // \"completed\" or \"failed\"\n */\n async commit(): Promise<Transaction<T>> {\n if (this.state !== `pending`) {\n throw `You can no longer call .commit() as the transaction is no longer pending`\n }\n\n this.setState(`persisting`)\n\n if (this.mutations.length === 0) {\n this.setState(`completed`)\n\n return this\n }\n\n // Run mutationFn\n try {\n // At this point we know there's at least one mutation\n // We've already verified mutations is non-empty, so this cast is safe\n // Use a direct type assertion instead of object spreading to preserve the original type\n await this.mutationFn({\n transaction: this as unknown as TransactionWithMutations<T>,\n })\n\n this.setState(`completed`)\n this.touchCollection()\n\n this.isPersisted.resolve(this)\n } catch (error) {\n // Update transaction with error information\n this.error = {\n message: error instanceof Error ? error.message : String(error),\n error: error instanceof Error ? error : new Error(String(error)),\n }\n\n // rollback the transaction\n return this.rollback()\n }\n\n return this\n }\n\n /**\n * Compare two transactions by their createdAt time and sequence number in order\n * to sort them in the order they were created.\n * @param other - The other transaction to compare to\n * @returns -1 if this transaction was created before the other, 1 if it was created after, 0 if they were created at the same time\n */\n compareCreatedAt(other: Transaction<any>): number {\n const createdAtComparison =\n this.createdAt.getTime() - other.createdAt.getTime()\n if (createdAtComparison !== 0) {\n return createdAtComparison\n }\n return this.sequenceNumber - other.sequenceNumber\n }\n}\n\nexport type { Transaction }\n"],"names":[],"mappings":"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"}
@@ -8,6 +8,24 @@ import { Transaction } from './transactions.js';
8
8
  * @internal This is used by the type resolution system
9
9
  */
10
10
  export type InferSchemaOutput<T> = T extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<T> extends object ? StandardSchemaV1.InferOutput<T> : Record<string, unknown> : Record<string, unknown>;
11
+ /**
12
+ * Helper type to extract the input type from a standard schema
13
+ *
14
+ * @internal This is used for collection insert type inference
15
+ */
16
+ export type InferSchemaInput<T> = T extends StandardSchemaV1 ? StandardSchemaV1.InferInput<T> extends object ? StandardSchemaV1.InferInput<T> : Record<string, unknown> : Record<string, unknown>;
17
+ /**
18
+ * Helper type to determine the insert input type
19
+ * This takes the raw generics (TExplicit, TSchema, TFallback) instead of the resolved T.
20
+ *
21
+ * Priority:
22
+ * 1. Explicit generic TExplicit (if not 'unknown')
23
+ * 2. Schema input type (if schema provided)
24
+ * 3. Fallback type TFallback
25
+ *
26
+ * @internal This is used for collection insert type inference
27
+ */
28
+ export type ResolveInsertInput<TExplicit = unknown, TSchema extends StandardSchemaV1 = never, TFallback extends object = Record<string, unknown>> = unknown extends TExplicit ? [TSchema] extends [never] ? TFallback : InferSchemaInput<TSchema> : TExplicit extends object ? TExplicit : Record<string, unknown>;
11
29
  /**
12
30
  * Helper type to determine the final type based on priority:
13
31
  * 1. Explicit generic TExplicit (if not 'unknown')
@@ -28,25 +46,32 @@ export type Fn = (...args: Array<any>) => any;
28
46
  * A record of utility functions that can be attached to a collection
29
47
  */
30
48
  export type UtilsRecord = Record<string, Fn>;
49
+ /**
50
+ *
51
+ * @remarks `update` and `insert` are both represented as `Partial<T>`, but changes for `insert` could me made more precise by inferring the schema input type. In practice, this has almost 0 real world impact so it's not worth the added type complexity.
52
+ *
53
+ * @see https://github.com/TanStack/db/pull/209#issuecomment-3053001206
54
+ */
55
+ export type ResolveTransactionChanges<T extends object = Record<string, unknown>, TOperation extends OperationType = OperationType> = TOperation extends `delete` ? T : Partial<T>;
31
56
  /**
32
57
  * Represents a pending mutation within a transaction
33
58
  * Contains information about the original and modified data, as well as metadata
34
59
  */
35
- export interface PendingMutation<T extends object = Record<string, unknown>, TOperation extends OperationType = OperationType> {
60
+ export interface PendingMutation<T extends object = Record<string, unknown>, TOperation extends OperationType = OperationType, TCollection extends Collection<T, any, any, any, any> = Collection<T, any, any, any, any>> {
36
61
  mutationId: string;
37
62
  original: TOperation extends `insert` ? {} : T;
38
63
  modified: T;
39
- changes: TOperation extends `insert` ? T : TOperation extends `delete` ? T : Partial<T>;
64
+ changes: ResolveTransactionChanges<T, TOperation>;
40
65
  globalKey: string;
41
66
  key: any;
42
- type: OperationType;
67
+ type: TOperation;
43
68
  metadata: unknown;
44
69
  syncMetadata: Record<string, unknown>;
45
70
  /** Whether this mutation should be applied optimistically (defaults to true) */
46
71
  optimistic: boolean;
47
72
  createdAt: Date;
48
73
  updatedAt: Date;
49
- collection: Collection<T, any, any>;
74
+ collection: TCollection;
50
75
  }
51
76
  /**
52
77
  * Configuration options for creating a new transaction
@@ -63,7 +88,7 @@ export type NonEmptyArray<T> = [T, ...Array<T>];
63
88
  * Utility type for a Transaction with at least one mutation
64
89
  * This is used internally by the Transaction.commit method
65
90
  */
66
- export type TransactionWithMutations<T extends object = Record<string, unknown>, TOperation extends OperationType = OperationType> = Transaction<T, TOperation> & {
91
+ export type TransactionWithMutations<T extends object = Record<string, unknown>, TOperation extends OperationType = OperationType> = Transaction<T> & {
67
92
  mutations: NonEmptyArray<PendingMutation<T, TOperation>>;
68
93
  };
69
94
  export interface TransactionConfig<T extends object = Record<string, unknown>> {
@@ -77,11 +102,11 @@ export interface TransactionConfig<T extends object = Record<string, unknown>> {
77
102
  /**
78
103
  * Options for the createOptimisticAction helper
79
104
  */
80
- export interface CreateOptimisticActionsOptions<TVars = unknown> extends Omit<TransactionConfig, `mutationFn`> {
105
+ export interface CreateOptimisticActionsOptions<TVars = unknown, T extends object = Record<string, unknown>> extends Omit<TransactionConfig<T>, `mutationFn`> {
81
106
  /** Function to apply optimistic updates locally before the mutation completes */
82
107
  onMutate: (vars: TVars) => void;
83
108
  /** Function to execute the mutation on the server */
84
- mutationFn: (vars: TVars, params: MutationFnParams) => Promise<any>;
109
+ mutationFn: (vars: TVars, params: MutationFnParams<T>) => Promise<any>;
85
110
  }
86
111
  export type { Transaction };
87
112
  type Value<TExtensions = never> = string | number | boolean | bigint | null | TExtensions | Array<Value<TExtensions>> | {
@@ -91,7 +116,7 @@ export type Row<TExtensions = never> = Record<string, Value<TExtensions>>;
91
116
  export type OperationType = `insert` | `update` | `delete`;
92
117
  export interface SyncConfig<T extends object = Record<string, unknown>, TKey extends string | number = string | number> {
93
118
  sync: (params: {
94
- collection: Collection<T, TKey>;
119
+ collection: Collection<T, TKey, any, any, any>;
95
120
  begin: () => void;
96
121
  write: (message: Omit<ChangeMessage<T>, `key`>) => void;
97
122
  commit: () => void;
@@ -189,7 +214,7 @@ export type CollectionStatus =
189
214
  | `error`
190
215
  /** Collection has been cleaned up and resources freed */
191
216
  | `cleaned-up`;
192
- export interface CollectionConfig<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TSchema extends StandardSchemaV1 = StandardSchemaV1> {
217
+ export interface CollectionConfig<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TSchema extends StandardSchemaV1 = StandardSchemaV1, TInsertInput extends object = T> {
193
218
  id?: string;
194
219
  sync: SyncConfig<T, TKey>;
195
220
  schema?: TSchema;
@@ -265,7 +290,7 @@ export interface CollectionConfig<T extends object = Record<string, unknown>, TK
265
290
  * })
266
291
  * }
267
292
  */
268
- onInsert?: InsertMutationFn<T, TKey>;
293
+ onInsert?: InsertMutationFn<TInsertInput, TKey>;
269
294
  /**
270
295
  * Optional asynchronous handler function called before an update operation
271
296
  * @param params Object containing transaction and collection information
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tanstack/db",
3
3
  "description": "A reactive client store for building super fast apps on sync",
4
- "version": "0.0.22",
4
+ "version": "0.0.23",
5
5
  "dependencies": {
6
6
  "@electric-sql/d2mini": "^0.1.7",
7
7
  "@standard-schema/spec": "^1.0.0"
package/src/collection.ts CHANGED
@@ -12,15 +12,17 @@ import type {
12
12
  OperationConfig,
13
13
  OptimisticChangeMessage,
14
14
  PendingMutation,
15
+ ResolveInsertInput,
15
16
  ResolveType,
16
17
  StandardSchema,
17
18
  Transaction as TransactionType,
19
+ TransactionWithMutations,
18
20
  UtilsRecord,
19
21
  } from "./types"
20
22
  import type { StandardSchemaV1 } from "@standard-schema/spec"
21
23
 
22
24
  // Store collections in memory
23
- export const collectionsStore = new Map<string, CollectionImpl<any, any>>()
25
+ export const collectionsStore = new Map<string, CollectionImpl<any, any, any>>()
24
26
 
25
27
  interface PendingSyncedTransaction<T extends object = Record<string, unknown>> {
26
28
  committed: boolean
@@ -32,12 +34,15 @@ interface PendingSyncedTransaction<T extends object = Record<string, unknown>> {
32
34
  * @template T - The type of items in the collection
33
35
  * @template TKey - The type of the key for the collection
34
36
  * @template TUtils - The utilities record type
37
+ * @template TInsertInput - The type for insert operations (can be different from T for schemas with defaults)
35
38
  */
36
39
  export interface Collection<
37
40
  T extends object = Record<string, unknown>,
38
41
  TKey extends string | number = string | number,
39
42
  TUtils extends UtilsRecord = {},
40
- > extends CollectionImpl<T, TKey> {
43
+ TSchema extends StandardSchemaV1 = StandardSchemaV1,
44
+ TInsertInput extends object = T,
45
+ > extends CollectionImpl<T, TKey, TUtils, TSchema, TInsertInput> {
41
46
  readonly utils: TUtils
42
47
  }
43
48
 
@@ -124,12 +129,22 @@ export function createCollection<
124
129
  options: CollectionConfig<
125
130
  ResolveType<TExplicit, TSchema, TFallback>,
126
131
  TKey,
127
- TSchema
132
+ TSchema,
133
+ ResolveInsertInput<TExplicit, TSchema, TFallback>
128
134
  > & { utils?: TUtils }
129
- ): Collection<ResolveType<TExplicit, TSchema, TFallback>, TKey, TUtils> {
135
+ ): Collection<
136
+ ResolveType<TExplicit, TSchema, TFallback>,
137
+ TKey,
138
+ TUtils,
139
+ TSchema,
140
+ ResolveInsertInput<TExplicit, TSchema, TFallback>
141
+ > {
130
142
  const collection = new CollectionImpl<
131
143
  ResolveType<TExplicit, TSchema, TFallback>,
132
- TKey
144
+ TKey,
145
+ TUtils,
146
+ TSchema,
147
+ ResolveInsertInput<TExplicit, TSchema, TFallback>
133
148
  >(options)
134
149
 
135
150
  // Copy utils to both top level and .utils namespace
@@ -142,7 +157,9 @@ export function createCollection<
142
157
  return collection as Collection<
143
158
  ResolveType<TExplicit, TSchema, TFallback>,
144
159
  TKey,
145
- TUtils
160
+ TUtils,
161
+ TSchema,
162
+ ResolveInsertInput<TExplicit, TSchema, TFallback>
146
163
  >
147
164
  }
148
165
 
@@ -179,8 +196,10 @@ export class CollectionImpl<
179
196
  T extends object = Record<string, unknown>,
180
197
  TKey extends string | number = string | number,
181
198
  TUtils extends UtilsRecord = {},
199
+ TSchema extends StandardSchemaV1 = StandardSchemaV1,
200
+ TInsertInput extends object = T,
182
201
  > {
183
- public config: CollectionConfig<T, TKey, any>
202
+ public config: CollectionConfig<T, TKey, TSchema, TInsertInput>
184
203
 
185
204
  // Core state - make public for testing
186
205
  public transactions: SortedMap<string, Transaction<any>>
@@ -312,7 +331,7 @@ export class CollectionImpl<
312
331
  * @param config - Configuration object for the collection
313
332
  * @throws Error if sync config is missing
314
333
  */
315
- constructor(config: CollectionConfig<T, TKey, any>) {
334
+ constructor(config: CollectionConfig<T, TKey, TSchema, TInsertInput>) {
316
335
  // eslint-disable-next-line
317
336
  if (!config) {
318
337
  throw new Error(`Collection requires a config`)
@@ -1322,9 +1341,11 @@ export class CollectionImpl<
1322
1341
  * console.log('Insert failed:', error)
1323
1342
  * }
1324
1343
  */
1325
- insert = (data: T | Array<T>, config?: InsertConfig) => {
1344
+ insert = (
1345
+ data: TInsertInput | Array<TInsertInput>,
1346
+ config?: InsertConfig
1347
+ ) => {
1326
1348
  this.validateCollectionUsable(`insert`)
1327
-
1328
1349
  const ambientTransaction = getActiveTransaction()
1329
1350
 
1330
1351
  // If no ambient transaction exists, check for an onInsert handler early
@@ -1335,7 +1356,7 @@ export class CollectionImpl<
1335
1356
  }
1336
1357
 
1337
1358
  const items = Array.isArray(data) ? data : [data]
1338
- const mutations: Array<PendingMutation<T, `insert`>> = []
1359
+ const mutations: Array<PendingMutation<T>> = []
1339
1360
 
1340
1361
  // Create mutations for each item
1341
1362
  items.forEach((item) => {
@@ -1343,7 +1364,7 @@ export class CollectionImpl<
1343
1364
  const validatedData = this.validateData(item, `insert`)
1344
1365
 
1345
1366
  // Check if an item with this ID already exists in the collection
1346
- const key = this.getKeyFromItem(item)
1367
+ const key = this.getKeyFromItem(validatedData)
1347
1368
  if (this.has(key)) {
1348
1369
  throw `Cannot insert document with ID "${key}" because it already exists in the collection`
1349
1370
  }
@@ -1353,7 +1374,15 @@ export class CollectionImpl<
1353
1374
  mutationId: crypto.randomUUID(),
1354
1375
  original: {},
1355
1376
  modified: validatedData,
1356
- changes: validatedData,
1377
+ // Pick the values from validatedData based on what's passed in - this is for cases
1378
+ // where a schema has default values. The validated data has the extra default
1379
+ // values but for changes, we just want to show the data that was actually passed in.
1380
+ changes: Object.fromEntries(
1381
+ Object.keys(item).map((k) => [
1382
+ k,
1383
+ validatedData[k as keyof typeof validatedData],
1384
+ ])
1385
+ ) as TInsertInput,
1357
1386
  globalKey,
1358
1387
  key,
1359
1388
  metadata: config?.metadata as unknown,
@@ -1381,8 +1410,12 @@ export class CollectionImpl<
1381
1410
  const directOpTransaction = createTransaction<T>({
1382
1411
  mutationFn: async (params) => {
1383
1412
  // Call the onInsert handler with the transaction and collection
1384
- return this.config.onInsert!({
1385
- ...params,
1413
+ return await this.config.onInsert!({
1414
+ transaction:
1415
+ params.transaction as unknown as TransactionWithMutations<
1416
+ TInsertInput,
1417
+ `insert`
1418
+ >,
1386
1419
  collection: this as unknown as Collection<T, TKey, TUtils>,
1387
1420
  })
1388
1421
  },
@@ -1526,7 +1559,7 @@ export class CollectionImpl<
1526
1559
  }
1527
1560
 
1528
1561
  // Create mutations for each object that has changes
1529
- const mutations: Array<PendingMutation<T, `update`>> = keysArray
1562
+ const mutations: Array<PendingMutation<T, `update`, this>> = keysArray
1530
1563
  .map((key, index) => {
1531
1564
  const itemChanges = changesArray[index] // User-provided changes for this specific item
1532
1565
 
@@ -1581,7 +1614,7 @@ export class CollectionImpl<
1581
1614
  collection: this,
1582
1615
  }
1583
1616
  })
1584
- .filter(Boolean) as Array<PendingMutation<T, `update`>>
1617
+ .filter(Boolean) as Array<PendingMutation<T, `update`, this>>
1585
1618
 
1586
1619
  // If no changes were made, return an empty transaction early
1587
1620
  if (mutations.length === 0) {
@@ -1609,7 +1642,11 @@ export class CollectionImpl<
1609
1642
  mutationFn: async (params) => {
1610
1643
  // Call the onUpdate handler with the transaction and collection
1611
1644
  return this.config.onUpdate!({
1612
- ...params,
1645
+ transaction:
1646
+ params.transaction as unknown as TransactionWithMutations<
1647
+ T,
1648
+ `update`
1649
+ >,
1613
1650
  collection: this as unknown as Collection<T, TKey, TUtils>,
1614
1651
  })
1615
1652
  },
@@ -1677,7 +1714,7 @@ export class CollectionImpl<
1677
1714
  }
1678
1715
 
1679
1716
  const keysArray = Array.isArray(keys) ? keys : [keys]
1680
- const mutations: Array<PendingMutation<T, `delete`>> = []
1717
+ const mutations: Array<PendingMutation<T, `delete`, this>> = []
1681
1718
 
1682
1719
  for (const key of keysArray) {
1683
1720
  if (!this.has(key)) {
@@ -1686,7 +1723,7 @@ export class CollectionImpl<
1686
1723
  )
1687
1724
  }
1688
1725
  const globalKey = this.generateGlobalKey(key, this.get(key)!)
1689
- const mutation: PendingMutation<T, `delete`> = {
1726
+ const mutation: PendingMutation<T, `delete`, this> = {
1690
1727
  mutationId: crypto.randomUUID(),
1691
1728
  original: this.get(key)!,
1692
1729
  modified: this.get(key)!,
@@ -1724,7 +1761,11 @@ export class CollectionImpl<
1724
1761
  mutationFn: async (params) => {
1725
1762
  // Call the onDelete handler with the transaction and collection
1726
1763
  return this.config.onDelete!({
1727
- ...params,
1764
+ transaction:
1765
+ params.transaction as unknown as TransactionWithMutations<
1766
+ T,
1767
+ `delete`
1768
+ >,
1728
1769
  collection: this as unknown as Collection<T, TKey, TUtils>,
1729
1770
  })
1730
1771
  },
@@ -393,7 +393,7 @@ export function localStorageCollectionOptions<
393
393
  // Remove items
394
394
  params.transaction.mutations.forEach((mutation) => {
395
395
  // For delete operations, mutation.original contains the full object
396
- const key = config.getKey(mutation.original)
396
+ const key = config.getKey(mutation.original as ResolvedType)
397
397
  currentData.delete(key)
398
398
  })
399
399
 
@@ -506,7 +506,7 @@ function createLocalStorageSync<T extends object>(
506
506
  storageKey: string,
507
507
  storage: StorageApi,
508
508
  storageEventApi: StorageEventApi,
509
- getKey: (item: T) => string | number,
509
+ _getKey: (item: T) => string | number,
510
510
  lastKnownData: Map<string | number, StoredItem<T>>
511
511
  ): SyncConfig<T> & { manualTrigger?: () => void } {
512
512
  let syncParams: Parameters<SyncConfig<T>[`sync`]>[0] | null = null
@@ -2,7 +2,6 @@ import { createDeferred } from "./deferred"
2
2
  import type { Deferred } from "./deferred"
3
3
  import type {
4
4
  MutationFn,
5
- OperationType,
6
5
  PendingMutation,
7
6
  TransactionConfig,
8
7
  TransactionState,
@@ -66,10 +65,10 @@ let sequenceNumber = 0
66
65
  * // Commit later
67
66
  * await tx.commit()
68
67
  */
69
- export function createTransaction<
70
- TData extends object = Record<string, unknown>,
71
- >(config: TransactionConfig<TData>): Transaction<TData> {
72
- const newTransaction = new Transaction<TData>(config)
68
+ export function createTransaction<T extends object = Record<string, unknown>>(
69
+ config: TransactionConfig<T>
70
+ ): Transaction<T> {
71
+ const newTransaction = new Transaction<T>(config)
73
72
  transactions.push(newTransaction)
74
73
  return newTransaction
75
74
  }
@@ -108,15 +107,12 @@ function removeFromPendingList(tx: Transaction<any>) {
108
107
  }
109
108
  }
110
109
 
111
- class Transaction<
112
- T extends object = Record<string, unknown>,
113
- TOperation extends OperationType = OperationType,
114
- > {
110
+ class Transaction<T extends object = Record<string, unknown>> {
115
111
  public id: string
116
112
  public state: TransactionState
117
113
  public mutationFn: MutationFn<T>
118
- public mutations: Array<PendingMutation<T, TOperation>>
119
- public isPersisted: Deferred<Transaction<T, TOperation>>
114
+ public mutations: Array<PendingMutation<T>>
115
+ public isPersisted: Deferred<Transaction<T>>
120
116
  public autoCommit: boolean
121
117
  public createdAt: Date
122
118
  public sequenceNumber: number
@@ -134,7 +130,7 @@ class Transaction<
134
130
  this.mutationFn = config.mutationFn
135
131
  this.state = `pending`
136
132
  this.mutations = []
137
- this.isPersisted = createDeferred<Transaction<T, TOperation>>()
133
+ this.isPersisted = createDeferred<Transaction<T>>()
138
134
  this.autoCommit = config.autoCommit ?? true
139
135
  this.createdAt = new Date()
140
136
  this.sequenceNumber = sequenceNumber++
package/src/types.ts CHANGED
@@ -14,6 +14,40 @@ export type InferSchemaOutput<T> = T extends StandardSchemaV1
14
14
  : Record<string, unknown>
15
15
  : Record<string, unknown>
16
16
 
17
+ /**
18
+ * Helper type to extract the input type from a standard schema
19
+ *
20
+ * @internal This is used for collection insert type inference
21
+ */
22
+ export type InferSchemaInput<T> = T extends StandardSchemaV1
23
+ ? StandardSchemaV1.InferInput<T> extends object
24
+ ? StandardSchemaV1.InferInput<T>
25
+ : Record<string, unknown>
26
+ : Record<string, unknown>
27
+
28
+ /**
29
+ * Helper type to determine the insert input type
30
+ * This takes the raw generics (TExplicit, TSchema, TFallback) instead of the resolved T.
31
+ *
32
+ * Priority:
33
+ * 1. Explicit generic TExplicit (if not 'unknown')
34
+ * 2. Schema input type (if schema provided)
35
+ * 3. Fallback type TFallback
36
+ *
37
+ * @internal This is used for collection insert type inference
38
+ */
39
+ export type ResolveInsertInput<
40
+ TExplicit = unknown,
41
+ TSchema extends StandardSchemaV1 = never,
42
+ TFallback extends object = Record<string, unknown>,
43
+ > = unknown extends TExplicit
44
+ ? [TSchema] extends [never]
45
+ ? TFallback
46
+ : InferSchemaInput<TSchema>
47
+ : TExplicit extends object
48
+ ? TExplicit
49
+ : Record<string, unknown>
50
+
17
51
  /**
18
52
  * Helper type to determine the final type based on priority:
19
53
  * 1. Explicit generic TExplicit (if not 'unknown')
@@ -48,6 +82,17 @@ export type Fn = (...args: Array<any>) => any
48
82
  */
49
83
  export type UtilsRecord = Record<string, Fn>
50
84
 
85
+ /**
86
+ *
87
+ * @remarks `update` and `insert` are both represented as `Partial<T>`, but changes for `insert` could me made more precise by inferring the schema input type. In practice, this has almost 0 real world impact so it's not worth the added type complexity.
88
+ *
89
+ * @see https://github.com/TanStack/db/pull/209#issuecomment-3053001206
90
+ */
91
+ export type ResolveTransactionChanges<
92
+ T extends object = Record<string, unknown>,
93
+ TOperation extends OperationType = OperationType,
94
+ > = TOperation extends `delete` ? T : Partial<T>
95
+
51
96
  /**
52
97
  * Represents a pending mutation within a transaction
53
98
  * Contains information about the original and modified data, as well as metadata
@@ -55,25 +100,32 @@ export type UtilsRecord = Record<string, Fn>
55
100
  export interface PendingMutation<
56
101
  T extends object = Record<string, unknown>,
57
102
  TOperation extends OperationType = OperationType,
103
+ TCollection extends Collection<T, any, any, any, any> = Collection<
104
+ T,
105
+ any,
106
+ any,
107
+ any,
108
+ any
109
+ >,
58
110
  > {
59
111
  mutationId: string
112
+ // The state of the object before the mutation.
60
113
  original: TOperation extends `insert` ? {} : T
114
+ // The result state of the object after all mutations.
61
115
  modified: T
62
- changes: TOperation extends `insert`
63
- ? T
64
- : TOperation extends `delete`
65
- ? T
66
- : Partial<T>
116
+ // Only the actual changes to the object by the mutation.
117
+ changes: ResolveTransactionChanges<T, TOperation>
67
118
  globalKey: string
119
+
68
120
  key: any
69
- type: OperationType
121
+ type: TOperation
70
122
  metadata: unknown
71
123
  syncMetadata: Record<string, unknown>
72
124
  /** Whether this mutation should be applied optimistically (defaults to true) */
73
125
  optimistic: boolean
74
126
  createdAt: Date
75
127
  updatedAt: Date
76
- collection: Collection<T, any, any>
128
+ collection: TCollection
77
129
  }
78
130
 
79
131
  /**
@@ -99,7 +151,7 @@ export type NonEmptyArray<T> = [T, ...Array<T>]
99
151
  export type TransactionWithMutations<
100
152
  T extends object = Record<string, unknown>,
101
153
  TOperation extends OperationType = OperationType,
102
- > = Transaction<T, TOperation> & {
154
+ > = Transaction<T> & {
103
155
  mutations: NonEmptyArray<PendingMutation<T, TOperation>>
104
156
  }
105
157
 
@@ -116,12 +168,14 @@ export interface TransactionConfig<T extends object = Record<string, unknown>> {
116
168
  /**
117
169
  * Options for the createOptimisticAction helper
118
170
  */
119
- export interface CreateOptimisticActionsOptions<TVars = unknown>
120
- extends Omit<TransactionConfig, `mutationFn`> {
171
+ export interface CreateOptimisticActionsOptions<
172
+ TVars = unknown,
173
+ T extends object = Record<string, unknown>,
174
+ > extends Omit<TransactionConfig<T>, `mutationFn`> {
121
175
  /** Function to apply optimistic updates locally before the mutation completes */
122
176
  onMutate: (vars: TVars) => void
123
177
  /** Function to execute the mutation on the server */
124
- mutationFn: (vars: TVars, params: MutationFnParams) => Promise<any>
178
+ mutationFn: (vars: TVars, params: MutationFnParams<T>) => Promise<any>
125
179
  }
126
180
 
127
181
  export type { Transaction }
@@ -145,7 +199,7 @@ export interface SyncConfig<
145
199
  TKey extends string | number = string | number,
146
200
  > {
147
201
  sync: (params: {
148
- collection: Collection<T, TKey>
202
+ collection: Collection<T, TKey, any, any, any>
149
203
  begin: () => void
150
204
  write: (message: Omit<ChangeMessage<T>, `key`>) => void
151
205
  commit: () => void
@@ -232,7 +286,6 @@ export type InsertMutationFnParams<
232
286
  transaction: TransactionWithMutations<T, `insert`>
233
287
  collection: Collection<T, TKey, TUtils>
234
288
  }
235
-
236
289
  export type DeleteMutationFnParams<
237
290
  T extends object = Record<string, unknown>,
238
291
  TKey extends string | number = string | number,
@@ -293,6 +346,7 @@ export interface CollectionConfig<
293
346
  T extends object = Record<string, unknown>,
294
347
  TKey extends string | number = string | number,
295
348
  TSchema extends StandardSchemaV1 = StandardSchemaV1,
349
+ TInsertInput extends object = T,
296
350
  > {
297
351
  // If an id isn't passed in, a UUID will be
298
352
  // generated for it.
@@ -371,7 +425,8 @@ export interface CollectionConfig<
371
425
  * })
372
426
  * }
373
427
  */
374
- onInsert?: InsertMutationFn<T, TKey>
428
+ onInsert?: InsertMutationFn<TInsertInput, TKey>
429
+
375
430
  /**
376
431
  * Optional asynchronous handler function called before an update operation
377
432
  * @param params Object containing transaction and collection information