Skip to content
Open
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
17 changes: 17 additions & 0 deletions .changeset/automatic-meta-type-safety.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@tanstack/query-db-collection": patch
---

fix: ensure ctx.meta.loadSubsetOptions type-safety works automatically

The module augmentation for ctx.meta.loadSubsetOptions is now guaranteed to load automatically when importing from @tanstack/query-db-collection. Previously, users needed to explicitly import QueryCollectionMeta or use @ts-ignore to pass ctx.meta?.loadSubsetOptions to parseLoadSubsetOptions.

Additionally, QueryCollectionMeta is now an interface (instead of a type alias), enabling users to safely extend meta with custom properties via declaration merging:

```typescript
declare module "@tanstack/query-db-collection" {
interface QueryCollectionMeta {
myCustomProperty: string
}
}
```
120 changes: 120 additions & 0 deletions docs/collections/query-collection.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,126 @@ The `queryCollectionOptions` function accepts the following options:
- `onUpdate`: Handler called before update operations
- `onDelete`: Handler called before delete operations

## Extending Meta with Custom Properties

The `meta` option allows you to pass additional metadata to your query function. By default, Query Collections automatically include `loadSubsetOptions` in the meta object, which contains filtering, sorting, and pagination options for on-demand queries.

### Type-Safe Meta Access

The `ctx.meta.loadSubsetOptions` property is automatically typed as `LoadSubsetOptions` without requiring any additional imports or type assertions:

```typescript
import { parseLoadSubsetOptions } from "@tanstack/query-db-collection"

const collection = createCollection(
queryCollectionOptions({
queryKey: ["products"],
syncMode: "on-demand",
queryFn: async (ctx) => {
// ✅ Type-safe access - no @ts-ignore needed!
const options = parseLoadSubsetOptions(ctx.meta?.loadSubsetOptions)

// Use the parsed options to fetch only what you need
return api.getProducts(options)
},
queryClient,
getKey: (item) => item.id,
})
)
```

### Adding Custom Meta Properties

You can extend the meta type to include your own custom properties using TypeScript's module augmentation:

```typescript
// In a global type definition file (e.g., types.d.ts or global.d.ts)
declare module "@tanstack/query-db-collection" {
interface QueryCollectionMeta {
// Add your custom properties here
userId?: string
includeDeleted?: boolean
cacheTTL?: number
}
}
```

Once you've extended the interface, your custom properties are fully typed throughout your application:

```typescript
const collection = createCollection(
queryCollectionOptions({
queryKey: ["todos"],
queryFn: async (ctx) => {
// ✅ Both loadSubsetOptions and custom properties are typed
const { loadSubsetOptions, userId, includeDeleted } = ctx.meta

return api.getTodos({
...parseLoadSubsetOptions(loadSubsetOptions),
userId,
includeDeleted,
})
},
queryClient,
getKey: (item) => item.id,
// Pass custom meta alongside Query Collection defaults
meta: {
userId: "user-123",
includeDeleted: false,
},
})
)
```

### Important Notes

- The module augmentation pattern follows TanStack Query's official approach for typing meta
- `QueryCollectionMeta` is an interface (not a type alias), enabling proper TypeScript declaration merging
- Your custom properties are merged with the base `loadSubsetOptions` property
- All meta properties must be compatible with `Record<string, unknown>`
- The augmentation should be done in a file that's included in your TypeScript compilation

### Example: API Request Context

A common use case is passing request context to your query function:

```typescript
// types.d.ts
declare module "@tanstack/query-db-collection" {
interface QueryCollectionMeta {
authToken?: string
locale?: string
version?: string
}
}

// collections.ts
const productsCollection = createCollection(
queryCollectionOptions({
queryKey: ["products"],
queryFn: async (ctx) => {
const { loadSubsetOptions, authToken, locale, version } = ctx.meta

return api.getProducts({
...parseLoadSubsetOptions(loadSubsetOptions),
headers: {
Authorization: `Bearer ${authToken}`,
"Accept-Language": locale,
"API-Version": version,
},
})
},
queryClient,
getKey: (item) => item.id,
meta: {
authToken: session.token,
locale: "en-US",
version: "v1",
},
})
)
```

## Persistence Handlers

You can define handlers that are called when mutations occur. These handlers can persist changes to your backend and control whether the query should refetch after the operation:
Expand Down
40 changes: 40 additions & 0 deletions packages/query-db-collection/src/global.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Global type augmentation for @tanstack/query-core
*
* This file ensures the module augmentation is always loaded when the package is imported.
* The index.ts file re-exports QueryCollectionMeta from this file, which guarantees
* TypeScript processes this file (and its module augmentation) whenever anyone imports
* from @tanstack/query-db-collection.
*
* This makes ctx.meta?.loadSubsetOptions automatically type-safe without requiring
* users to manually import QueryCollectionMeta.
*/

import type { LoadSubsetOptions } from "@tanstack/db"

/**
* Base interface for Query Collection meta properties.
* Users can extend this interface to add their own custom properties while
* preserving loadSubsetOptions.
*
* @example
* ```typescript
* declare module "@tanstack/query-db-collection" {
* interface QueryCollectionMeta {
* myCustomProperty: string
* userId?: number
* }
* }
* ```
*/
export interface QueryCollectionMeta extends Record<string, unknown> {
loadSubsetOptions: LoadSubsetOptions
}

// Module augmentation to extend TanStack Query's Register interface
// This ensures that ctx.meta always includes loadSubsetOptions
declare module "@tanstack/query-core" {
interface Register {
queryMeta: QueryCollectionMeta
}
}
5 changes: 4 additions & 1 deletion packages/query-db-collection/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
// Export QueryCollectionMeta from global.ts
// This ensures the module augmentation in global.ts is processed by TypeScript
export type { QueryCollectionMeta } from "./global"

export {
queryCollectionOptions,
type QueryCollectionConfig,
type QueryCollectionMeta,
type QueryCollectionUtils,
type SyncOperation,
} from "./query"
Expand Down
29 changes: 0 additions & 29 deletions packages/query-db-collection/src/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,35 +31,6 @@ import type { StandardSchemaV1 } from "@standard-schema/spec"
// Re-export for external use
export type { SyncOperation } from "./manual-sync"

/**
* Base type for Query Collection meta properties.
* Users can extend this type when augmenting the @tanstack/query-core module
* to add their own custom properties while preserving loadSubsetOptions.
*
* @example
* ```typescript
* declare module "@tanstack/query-core" {
* interface Register {
* queryMeta: QueryCollectionMeta & {
* myCustomProperty: string
* }
* }
* }
* ```
*/
export type QueryCollectionMeta = Record<string, unknown> & {
loadSubsetOptions: LoadSubsetOptions
}

// Module augmentation to extend TanStack Query's Register interface
// This ensures that ctx.meta always includes loadSubsetOptions
// We extend Record<string, unknown> to preserve the ability to add other meta properties
declare module "@tanstack/query-core" {
interface Register {
queryMeta: QueryCollectionMeta
}
}

// Schema output type inference helper (matches electric.ts pattern)
type InferSchemaOutput<T> = T extends StandardSchemaV1
? StandardSchemaV1.InferOutput<T> extends object
Expand Down
71 changes: 71 additions & 0 deletions packages/query-db-collection/tests/query.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,5 +470,76 @@ describe(`Query collection type resolution tests`, () => {
const options = queryCollectionOptions(config)
createCollection(options)
})

it(`should have loadSubsetOptions typed automatically without explicit QueryCollectionMeta import`, () => {
// This test validates that the module augmentation works automatically
// Note: We are NOT importing QueryCollectionMeta, yet ctx.meta.loadSubsetOptions
// should still be properly typed as LoadSubsetOptions
const config: QueryCollectionConfig<TestItem> = {
id: `autoTypeTest`,
queryClient,
queryKey: [`autoTypeTest`],
queryFn: (ctx) => {
// This should compile without errors because the module augmentation
// in global.d.ts is automatically loaded via the triple-slash reference
// in index.ts
const options = ctx.meta?.loadSubsetOptions

// Verify the type is correct
expectTypeOf(options).toMatchTypeOf<LoadSubsetOptions | undefined>()

// Verify it can be passed to parseLoadSubsetOptions without type errors
const parsed = parseLoadSubsetOptions(options)
expectTypeOf(parsed).toMatchTypeOf<{
filters: Array<any>
sorts: Array<any>
limit?: number
}>()

return Promise.resolve([])
},
getKey: (item) => item.id,
syncMode: `on-demand`,
}

const options = queryCollectionOptions(config)
createCollection(options)
})

it(`should allow users to extend QueryCollectionMeta via module augmentation`, () => {
// This test validates that users can extend QueryCollectionMeta to add custom properties
// by augmenting the @tanstack/query-db-collection module

// In reality, users would do:
// declare module "@tanstack/query-db-collection" {
// interface QueryCollectionMeta {
// customUserId: number
// customContext?: string
// }
// }

const config: QueryCollectionConfig<TestItem> = {
id: `extendMetaTest`,
queryClient,
queryKey: [`extendMetaTest`],
queryFn: (ctx) => {
// ctx.meta still has loadSubsetOptions
expectTypeOf(ctx.meta?.loadSubsetOptions).toMatchTypeOf<
LoadSubsetOptions | undefined
>()

// This test documents the extension pattern even though we can't
// actually augment QueryCollectionMeta in a test file (it would
// affect all other tests in the same compilation unit)

return Promise.resolve([])
},
getKey: (item) => item.id,
syncMode: `on-demand`,
}

const options = queryCollectionOptions(config)
createCollection(options)
})
})
})
Loading