Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/evil-regions-stop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@tanstack/trailbase-db-collection": patch
"@tanstack/electric-db-collection": patch
"@tanstack/query-db-collection": patch
"@tanstack/db": patch
---

Define BaseCollectionConfig interface and let all collections extend it.
40 changes: 5 additions & 35 deletions packages/db/src/local-only.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {
BaseCollectionConfig,
CollectionConfig,
DeleteMutationFnParams,
InferSchemaOutput,
Expand All @@ -20,46 +21,15 @@ export interface LocalOnlyCollectionConfig<
T extends object = object,
TSchema extends StandardSchemaV1 = never,
TKey extends string | number = string | number,
> {
/**
* Standard Collection configuration properties
*/
id?: string
schema?: TSchema
getKey: (item: T) => TKey

> extends Omit<
BaseCollectionConfig<T, TKey, TSchema, LocalOnlyCollectionUtils>,
`gcTime` | `startSync`
> {
/**
* Optional initial data to populate the collection with on creation
* This data will be applied during the initial sync process
*/
initialData?: Array<T>

/**
* Optional asynchronous handler function called after an insert operation
* @param params Object containing transaction and collection information
* @returns Promise resolving to any value
*/
onInsert?: (
params: InsertMutationFnParams<T, TKey, LocalOnlyCollectionUtils>
) => Promise<any>

/**
* Optional asynchronous handler function called after an update operation
* @param params Object containing transaction and collection information
* @returns Promise resolving to any value
*/
onUpdate?: (
params: UpdateMutationFnParams<T, TKey, LocalOnlyCollectionUtils>
) => Promise<any>

/**
* Optional asynchronous handler function called after a delete operation
* @param params Object containing transaction and collection information
* @returns Promise resolving to any value
*/
onDelete?: (
params: DeleteMutationFnParams<T, TKey, LocalOnlyCollectionUtils>
) => Promise<any>
}

/**
Expand Down
32 changes: 2 additions & 30 deletions packages/db/src/local-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
StorageKeyRequiredError,
} from "./errors"
import type {
BaseCollectionConfig,
CollectionConfig,
DeleteMutationFnParams,
InferSchemaOutput,
Expand Down Expand Up @@ -54,7 +55,7 @@ export interface LocalStorageCollectionConfig<
T extends object = object,
TSchema extends StandardSchemaV1 = never,
TKey extends string | number = string | number,
> {
> extends BaseCollectionConfig<T, TKey, TSchema> {
/**
* The key to use for storing the collection data in localStorage/sessionStorage
*/
Expand All @@ -71,35 +72,6 @@ export interface LocalStorageCollectionConfig<
* Can be any object that implements addEventListener/removeEventListener for storage events
*/
storageEventApi?: StorageEventApi

/**
* Collection identifier (defaults to "local-collection:{storageKey}" if not provided)
*/
id?: string
schema?: TSchema
getKey: CollectionConfig<T, TKey, TSchema>[`getKey`]
sync?: CollectionConfig<T, TKey, TSchema>[`sync`]

/**
* Optional asynchronous handler function called before an insert operation
* @param params Object containing transaction and collection information
* @returns Promise resolving to any value
*/
onInsert?: (params: InsertMutationFnParams<T>) => Promise<any>

/**
* Optional asynchronous handler function called before an update operation
* @param params Object containing transaction and collection information
* @returns Promise resolving to any value
*/
onUpdate?: (params: UpdateMutationFnParams<T>) => Promise<any>

/**
* Optional asynchronous handler function called before a delete operation
* @param params Object containing transaction and collection information
* @returns Promise resolving to any value
*/
onDelete?: (params: DeleteMutationFnParams<T>) => Promise<any>
}

/**
Expand Down
28 changes: 20 additions & 8 deletions packages/db/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,19 +259,22 @@ export type InsertMutationFn<
T extends object = Record<string, unknown>,
TKey extends string | number = string | number,
TUtils extends UtilsRecord = Record<string, Fn>,
> = (params: InsertMutationFnParams<T, TKey, TUtils>) => Promise<any>
TReturn = any,
> = (params: InsertMutationFnParams<T, TKey, TUtils>) => Promise<TReturn>

export type UpdateMutationFn<
T extends object = Record<string, unknown>,
TKey extends string | number = string | number,
TUtils extends UtilsRecord = Record<string, Fn>,
> = (params: UpdateMutationFnParams<T, TKey, TUtils>) => Promise<any>
TReturn = any,
> = (params: UpdateMutationFnParams<T, TKey, TUtils>) => Promise<TReturn>

export type DeleteMutationFn<
T extends object = Record<string, unknown>,
TKey extends string | number = string | number,
TUtils extends UtilsRecord = Record<string, Fn>,
> = (params: DeleteMutationFnParams<T, TKey, TUtils>) => Promise<any>
TReturn = any,
> = (params: DeleteMutationFnParams<T, TKey, TUtils>) => Promise<TReturn>

/**
* Collection status values for lifecycle management
Expand Down Expand Up @@ -302,19 +305,20 @@ export type CollectionStatus =
/** Collection has been cleaned up and resources freed */
| `cleaned-up`

export interface CollectionConfig<
export interface BaseCollectionConfig<
T extends object = Record<string, unknown>,
TKey extends string | number = string | number,
// Let TSchema default to `never` such that if a user provides T explicitly and no schema
// then TSchema will be `never` otherwise if it would default to StandardSchemaV1
// then it would conflict with the overloads of createCollection which
// requires either T to be provided or a schema to be provided but not both!
TSchema extends StandardSchemaV1 = never,
TUtils extends UtilsRecord = Record<string, Fn>,
TReturn = any,
> {
// If an id isn't passed in, a UUID will be
// generated for it.
id?: string
sync: SyncConfig<T, TKey>
schema?: TSchema
/**
* Function to extract the ID from an object
Expand Down Expand Up @@ -397,7 +401,7 @@ export interface CollectionConfig<
* })
* }
*/
onInsert?: InsertMutationFn<T, TKey>
onInsert?: InsertMutationFn<T, TKey, TUtils, TReturn>

/**
* Optional asynchronous handler function called before an update operation
Expand Down Expand Up @@ -441,7 +445,7 @@ export interface CollectionConfig<
* }
* }
*/
onUpdate?: UpdateMutationFn<T, TKey>
onUpdate?: UpdateMutationFn<T, TKey, TUtils, TReturn>
/**
* Optional asynchronous handler function called before a delete operation
* @param params Object containing transaction and collection information
Expand Down Expand Up @@ -484,7 +488,15 @@ export interface CollectionConfig<
* }
* }
*/
onDelete?: DeleteMutationFn<T, TKey>
onDelete?: DeleteMutationFn<T, TKey, TUtils, TReturn>
}

export interface CollectionConfig<
T extends object = Record<string, unknown>,
TKey extends string | number = string | number,
TSchema extends StandardSchemaV1 = never,
> extends BaseCollectionConfig<T, TKey, TSchema> {
sync: SyncConfig<T, TKey>
}

export type ChangesPayload<T extends object = Record<string, unknown>> = Array<
Expand Down
175 changes: 9 additions & 166 deletions packages/electric-db-collection/src/electric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import {
TimeoutWaitingForTxIdError,
} from "./errors"
import type {
BaseCollectionConfig,
CollectionConfig,
DeleteMutationFnParams,
Fn,
InsertMutationFnParams,
SyncConfig,
UpdateMutationFnParams,
Expand Down Expand Up @@ -53,176 +55,17 @@ type InferSchemaOutput<T> = T extends StandardSchemaV1
export interface ElectricCollectionConfig<
T extends Row<unknown> = Row<unknown>,
TSchema extends StandardSchemaV1 = never,
> {
> extends BaseCollectionConfig<
T,
string | number,
TSchema,
Record<string, Fn>,
{ txid: Txid | Array<Txid> }
> {
/**
* Configuration options for the ElectricSQL ShapeStream
*/
shapeOptions: ShapeStreamOptions<GetExtensions<T>>

/**
* All standard Collection configuration properties
*/
id?: string
schema?: TSchema
getKey: CollectionConfig<T, string | number, TSchema>[`getKey`]
sync?: CollectionConfig<T, string | number, TSchema>[`sync`]

/**
* Optional asynchronous handler function called before an insert operation
* Must return an object containing a txid number or array of txids
* @param params Object containing transaction and collection information
* @returns Promise resolving to an object with txid or txids
* @example
* // Basic Electric insert handler - MUST return { txid: number }
* onInsert: async ({ transaction }) => {
* const newItem = transaction.mutations[0].modified
* const result = await api.todos.create({
* data: newItem
* })
* return { txid: result.txid } // Required for Electric sync matching
* }
*
* @example
* // Insert handler with multiple items - return array of txids
* onInsert: async ({ transaction }) => {
* const items = transaction.mutations.map(m => m.modified)
* const results = await Promise.all(
* items.map(item => api.todos.create({ data: item }))
* )
* return { txid: results.map(r => r.txid) } // Array of txids
* }
*
* @example
* // Insert handler with error handling
* onInsert: async ({ transaction }) => {
* try {
* const newItem = transaction.mutations[0].modified
* const result = await api.createTodo(newItem)
* return { txid: result.txid }
* } catch (error) {
* console.error('Insert failed:', error)
* throw error // This will cause the transaction to fail
* }
* }
*
* @example
* // Insert handler with batch operation - single txid
* onInsert: async ({ transaction }) => {
* const items = transaction.mutations.map(m => m.modified)
* const result = await api.todos.createMany({
* data: items
* })
* return { txid: result.txid } // Single txid for batch operation
* }
*/
onInsert?: (
params: InsertMutationFnParams<T>
) => Promise<{ txid: Txid | Array<Txid> }>

/**
* Optional asynchronous handler function called before an update operation
* Must return an object containing a txid number or array of txids
* @param params Object containing transaction and collection information
* @returns Promise resolving to an object with txid or txids
* @example
* // Basic Electric update handler - MUST return { txid: number }
* onUpdate: async ({ transaction }) => {
* const { original, changes } = transaction.mutations[0]
* const result = await api.todos.update({
* where: { id: original.id },
* data: changes // Only the changed fields
* })
* return { txid: result.txid } // Required for Electric sync matching
* }
*
* @example
* // Update handler with multiple items - return array of txids
* onUpdate: async ({ transaction }) => {
* const updates = await Promise.all(
* transaction.mutations.map(m =>
* api.todos.update({
* where: { id: m.original.id },
* data: m.changes
* })
* )
* )
* return { txid: updates.map(u => u.txid) } // Array of txids
* }
*
* @example
* // Update handler with optimistic rollback
* onUpdate: async ({ transaction }) => {
* const mutation = transaction.mutations[0]
* try {
* const result = await api.updateTodo(mutation.original.id, mutation.changes)
* return { txid: result.txid }
* } catch (error) {
* // Transaction will automatically rollback optimistic changes
* console.error('Update failed, rolling back:', error)
* throw error
* }
* }
*/
onUpdate?: (
params: UpdateMutationFnParams<T>
) => Promise<{ txid: Txid | Array<Txid> }>

/**
* Optional asynchronous handler function called before a delete operation
* Must return an object containing a txid number or array of txids
* @param params Object containing transaction and collection information
* @returns Promise resolving to an object with txid or txids
* @example
* // Basic Electric delete handler - MUST return { txid: number }
* onDelete: async ({ transaction }) => {
* const mutation = transaction.mutations[0]
* const result = await api.todos.delete({
* id: mutation.original.id
* })
* return { txid: result.txid } // Required for Electric sync matching
* }
*
* @example
* // Delete handler with multiple items - return array of txids
* onDelete: async ({ transaction }) => {
* const deletes = await Promise.all(
* transaction.mutations.map(m =>
* api.todos.delete({
* where: { id: m.key }
* })
* )
* )
* return { txid: deletes.map(d => d.txid) } // Array of txids
* }
*
* @example
* // Delete handler with batch operation - single txid
* onDelete: async ({ transaction }) => {
* const idsToDelete = transaction.mutations.map(m => m.original.id)
* const result = await api.todos.deleteMany({
* ids: idsToDelete
* })
* return { txid: result.txid } // Single txid for batch operation
* }
*
* @example
* // Delete handler with optimistic rollback
* onDelete: async ({ transaction }) => {
* const mutation = transaction.mutations[0]
* try {
* const result = await api.deleteTodo(mutation.original.id)
* return { txid: result.txid }
* } catch (error) {
* // Transaction will automatically rollback optimistic changes
* console.error('Delete failed, rolling back:', error)
* throw error
* }
* }
*
*/
onDelete?: (
params: DeleteMutationFnParams<T>
) => Promise<{ txid: Txid | Array<Txid> }>
}

function isUpToDateMessage<T extends Row<unknown>>(
Expand Down
Loading
Loading