diff --git a/.changeset/fix-query-schema-select-output.md b/.changeset/fix-query-schema-select-output.md new file mode 100644 index 0000000000..3a8d9d6051 --- /dev/null +++ b/.changeset/fix-query-schema-select-output.md @@ -0,0 +1,5 @@ +--- +'@tanstack/query-db-collection': patch +--- + +Fix `queryCollectionOptions` to accept selectors that materialize transforming schema outputs from wrapped responses while preserving schema input types for mutations. diff --git a/packages/query-db-collection/src/query.ts b/packages/query-db-collection/src/query.ts index 14b81e0d20..c924f292e3 100644 --- a/packages/query-db-collection/src/query.ts +++ b/packages/query-db-collection/src/query.ts @@ -562,16 +562,20 @@ export function queryCollectionOptions< TKey extends string | number = string | number, TQueryData = Awaited>, >( - config: QueryCollectionConfig< - InferSchemaOutput, - TQueryFn, - TError, - TQueryKey, - TKey, - T + config: Omit< + QueryCollectionConfig< + InferSchemaOutput, + TQueryFn, + TError, + TQueryKey, + TKey, + T, + TQueryData + >, + `select` > & { schema: T - select: (data: TQueryData) => Array> + select: (data: TQueryData) => Array> }, ): CollectionConfig< InferSchemaOutput, diff --git a/packages/query-db-collection/tests/query.test-d.ts b/packages/query-db-collection/tests/query.test-d.ts index ebb1e13e71..bc9cb74041 100644 --- a/packages/query-db-collection/tests/query.test-d.ts +++ b/packages/query-db-collection/tests/query.test-d.ts @@ -488,6 +488,97 @@ describe(`Query collection type resolution tests`, () => { // Should infer ResponseType as select parameter type expectTypeOf(selectUserData).parameters.toEqualTypeOf<[ResponseType]>() }) + + /** + * Law and source: The public schema-plus-select overload maps the `queryFn` + * result through `select` into materialized schema output rows. Collection + * mutations still accept schema input. Here, the schema relation is + * `createdAt: string` input -> `createdAt: Date` output. + * + * Production path and checkpoint: Infer options through + * `queryCollectionOptions`, create the Collection, then inspect the public + * types at compile time. + * + * Observations: `select` input, output-row keys, delete keys, collection + * reads, and mutation inputs. The paired configs are the hostile control: + * they differ only in whether `select` returns schema output or input. + * Runtime schema parsing and result publication are outside this oracle. + */ + it(`preserves schema output rows selected from a wrapped response`, () => { + const rowSchema = z.object({ + id: z.string(), + createdAt: z.string().transform((value) => new Date(value)), + }) + + type RowInput = z.input + type RowOutput = z.output + type WrappedResponse = { + rows: Array + total: number + } + + const options = queryCollectionOptions({ + queryClient, + queryKey: [`wrapped-transformed-schema`], + queryFn: async (): Promise => ({ + rows: [{ id: `1`, createdAt: new Date(0) }], + total: 1, + }), + select: (response) => { + expectTypeOf(response).toEqualTypeOf() + return response.rows + }, + schema: rowSchema, + getKey: (item) => item.id, + }) + + expectTypeOf(options.getKey).parameters.toEqualTypeOf<[RowOutput]>() + expectTypeOf(options.getKey).returns.toEqualTypeOf() + expectTypeOf(options.utils.writeDelete).parameters.toEqualTypeOf< + [string | Array] + >() + + const collection = createCollection(options) + collection.insert({ id: `2`, createdAt: `2026-09-20T00:00:00.000Z` }) + + const selected = collection.get(`1`) + if (selected) { + expectTypeOf(selected.createdAt).toEqualTypeOf() + } + + // @ts-expect-error schema mutation inputs have not been materialized yet + collection.insert({ id: `3`, createdAt: new Date(0) }) + + type HostileResponse = { + inputRows: Array + outputRows: Array + } + + // Keep every option shared so unrelated config errors cannot satisfy the + // negative assertion at the public overload. + const schemaOutputSelectConfig = { + queryClient, + queryKey: [`wrapped-schema-output-control`], + queryFn: async (): Promise => ({ + inputRows: [{ id: `1`, createdAt: `1970-01-01T00:00:00.000Z` }], + outputRows: [{ id: `1`, createdAt: new Date(0) }], + }), + select: (response: HostileResponse): Array => + response.outputRows, + schema: rowSchema, + getKey: (item: RowOutput) => item.id, + } + queryCollectionOptions(schemaOutputSelectConfig) + + const schemaInputSelectConfig = { + ...schemaOutputSelectConfig, + select: (response: HostileResponse): Array => + response.inputRows, + } + + // @ts-expect-error select must return materialized schema output rows + queryCollectionOptions(schemaInputSelectConfig) + }) }) describe(`loadSubsetOptions type inference`, () => {