diff --git a/.changeset/eighty-ideas-clean.md b/.changeset/eighty-ideas-clean.md new file mode 100644 index 000000000..c0eb9abe5 --- /dev/null +++ b/.changeset/eighty-ideas-clean.md @@ -0,0 +1,5 @@ +--- +"@tanstack/query-db-collection": patch +--- + +Improved the type of the queryFn's ctx.meta property of the Query Collection to include the loadSubsetOptions diff --git a/packages/query-db-collection/e2e/query.e2e.test.ts b/packages/query-db-collection/e2e/query.e2e.test.ts index c93b532c0..7e31efdc6 100644 --- a/packages/query-db-collection/e2e/query.e2e.test.ts +++ b/packages/query-db-collection/e2e/query.e2e.test.ts @@ -19,7 +19,6 @@ import { generateSeedData, } from "../../db-collection-e2e/src/index" import { applyPredicates, buildQueryKey } from "./query-filter" -import type { LoadSubsetOptions } from "@tanstack/db" import type { Comment as E2EComment, Post as E2EPost, @@ -94,9 +93,7 @@ describe(`Query Collection E2E Tests`, () => { queryKey: (opts) => buildQueryKey(`users`, opts), syncMode: `on-demand`, queryFn: (ctx) => { - const options = ctx.meta?.loadSubsetOptions as - | LoadSubsetOptions - | undefined + const options = ctx.meta?.loadSubsetOptions const filtered = applyPredicates(seedData.users, options) return Promise.resolve(filtered) }, @@ -112,9 +109,7 @@ describe(`Query Collection E2E Tests`, () => { queryKey: (opts) => buildQueryKey(`posts`, opts), syncMode: `on-demand`, queryFn: (ctx) => { - const options = ctx.meta?.loadSubsetOptions as - | LoadSubsetOptions - | undefined + const options = ctx.meta?.loadSubsetOptions const filtered = applyPredicates(seedData.posts, options) return Promise.resolve(filtered) }, @@ -130,9 +125,7 @@ describe(`Query Collection E2E Tests`, () => { queryKey: (opts) => buildQueryKey(`comments`, opts), syncMode: `on-demand`, queryFn: (ctx) => { - const options = ctx.meta?.loadSubsetOptions as - | LoadSubsetOptions - | undefined + const options = ctx.meta?.loadSubsetOptions const filtered = applyPredicates(seedData.comments, options) return Promise.resolve(filtered) }, diff --git a/packages/query-db-collection/src/index.ts b/packages/query-db-collection/src/index.ts index 1a3169b3f..989f15487 100644 --- a/packages/query-db-collection/src/index.ts +++ b/packages/query-db-collection/src/index.ts @@ -1,6 +1,7 @@ export { queryCollectionOptions, type QueryCollectionConfig, + type QueryCollectionMeta, type QueryCollectionUtils, type SyncOperation, } from "./query" diff --git a/packages/query-db-collection/src/query.ts b/packages/query-db-collection/src/query.ts index d5c469615..36c9e45d0 100644 --- a/packages/query-db-collection/src/query.ts +++ b/packages/query-db-collection/src/query.ts @@ -31,6 +31,35 @@ 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 & { + loadSubsetOptions: LoadSubsetOptions +} + +// Module augmentation to extend TanStack Query's Register interface +// This ensures that ctx.meta always includes loadSubsetOptions +// We extend Record 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 extends StandardSchemaV1 ? StandardSchemaV1.InferOutput extends object diff --git a/packages/query-db-collection/tests/query.test-d.ts b/packages/query-db-collection/tests/query.test-d.ts index c8df9298e..b4f5140b0 100644 --- a/packages/query-db-collection/tests/query.test-d.ts +++ b/packages/query-db-collection/tests/query.test-d.ts @@ -5,13 +5,16 @@ import { createLiveQueryCollection, eq, gt, + parseLoadSubsetOptions, } from "@tanstack/db" import { QueryClient } from "@tanstack/query-core" import { z } from "zod" import { queryCollectionOptions } from "../src/query" +import type { QueryCollectionConfig } from "../src/query" import type { DeleteMutationFnParams, InsertMutationFnParams, + LoadSubsetOptions, UpdateMutationFnParams, } from "@tanstack/db" @@ -403,4 +406,69 @@ describe(`Query collection type resolution tests`, () => { expectTypeOf(selectUserData).parameters.toEqualTypeOf<[ResponseType]>() }) }) + + describe(`loadSubsetOptions type inference`, () => { + interface TestItem { + id: string + name: string + } + + it(`should type loadSubsetOptions as LoadSubsetOptions in queryFn`, () => { + const config: QueryCollectionConfig = { + id: `loadSubsetTest`, + queryClient, + queryKey: [`loadSubsetTest`], + queryFn: (ctx) => { + // Verify that loadSubsetOptions is assignable to LoadSubsetOptions + // This ensures it can be used where LoadSubsetOptions is expected + expectTypeOf( + ctx.meta!.loadSubsetOptions + ).toExtend() + // so that parseLoadSubsetOptions can be called without type errors + parseLoadSubsetOptions(ctx.meta?.loadSubsetOptions) + // The fact that this call compiles without errors verifies that + // ctx.meta.loadSubsetOptions is typed correctly as LoadSubsetOptions + return Promise.resolve([]) + }, + getKey: (item) => item.id, + syncMode: `on-demand`, + } + + const options = queryCollectionOptions(config) + createCollection(options) + }) + + it(`should allow meta to contain additional properties beyond loadSubsetOptions`, () => { + const config: QueryCollectionConfig = { + id: `loadSubsetTest`, + queryClient, + queryKey: [`loadSubsetTest`], + queryFn: (ctx) => { + // Verify that an object with loadSubsetOptions plus other properties + // can be assigned to ctx.meta's type. This ensures the type is not too restrictive. + const metaWithExtra = { + loadSubsetOptions: ctx.meta!.loadSubsetOptions, + customProperty: `test`, + anotherProperty: 123, + } + + // Test that this object can be assigned to ctx.meta's type + // This verifies that ctx.meta allows additional properties beyond loadSubsetOptions + const typedMeta: typeof ctx.meta = metaWithExtra + + // Verify the assignment worked (this will fail at compile time if types don't match) + expectTypeOf( + typedMeta.loadSubsetOptions + ).toExtend() + + return Promise.resolve([]) + }, + getKey: (item) => item.id, + syncMode: `on-demand`, + } + + const options = queryCollectionOptions(config) + createCollection(options) + }) + }) }) diff --git a/packages/query-db-collection/tests/query.test.ts b/packages/query-db-collection/tests/query.test.ts index a1bdced77..e0807232e 100644 --- a/packages/query-db-collection/tests/query.test.ts +++ b/packages/query-db-collection/tests/query.test.ts @@ -13,7 +13,6 @@ import type { Collection, DeleteMutationFnParams, InsertMutationFnParams, - LoadSubsetOptions, TransactionWithMutations, UpdateMutationFnParams, } from "@tanstack/db" @@ -455,9 +454,7 @@ describe(`QueryCollection`, () => { const queryFn = vi .fn() .mockImplementation((ctx: QueryFunctionContext) => { - const loadSubsetOptions = ctx.meta?.loadSubsetOptions as - | LoadSubsetOptions - | undefined + const loadSubsetOptions = ctx.meta?.loadSubsetOptions // Verify where clause is present expect(loadSubsetOptions?.where).toBeDefined() expect(loadSubsetOptions?.where).not.toBeNull() @@ -515,9 +512,7 @@ describe(`QueryCollection`, () => { const queryFn = vi .fn() .mockImplementation((ctx: QueryFunctionContext) => { - const loadSubsetOptions = ctx.meta?.loadSubsetOptions as - | LoadSubsetOptions - | undefined + const loadSubsetOptions = ctx.meta?.loadSubsetOptions // Verify where clause is present (this was the bug - it was undefined/null before the fix) expect(loadSubsetOptions?.where).toBeDefined() expect(loadSubsetOptions?.where).not.toBeNull() @@ -3678,9 +3673,7 @@ describe(`QueryCollection`, () => { ] const queryFn = vi.fn((ctx: QueryFunctionContext) => { - const loadSubsetOptions = ctx.meta?.loadSubsetOptions as - | LoadSubsetOptions - | undefined + const loadSubsetOptions = ctx.meta?.loadSubsetOptions // Filter items based on the where clause if present if (loadSubsetOptions?.where) { // Simple mock filtering - in real use, you'd use parseLoadSubsetOptions