From ab755e23d0ef3a846f25cc3be038f9f6e018666f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 05:59:14 +0000 Subject: [PATCH 1/5] fix: let object overrides beat function base modifiers in query utils When the base modifier was a function and the override a plain object, `mergeProcedureUtilsModifier` applied the override first and the base function last, so every key the base set beat the override. With two objects the override won, so priority depended on how the lower layer was written (e.g. a function `queryOptions` in contract meta beat an object `queryOptions` on the utils). Now an object override is fed to a function base as defaults under the per-call options, giving per-call options > override > base defaults. Applied to both @orpc/tanstack-query and @orpc/pinia-colada. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01B5V7qjA3f5ancGzD2FDQEF --- packages/pinia-colada/src/meta.test.ts | 30 +++++++++++++++++++ .../pinia-colada/src/procedure-utils.test.ts | 10 +++++++ packages/pinia-colada/src/procedure-utils.ts | 18 ++++++----- packages/tanstack-query/src/meta.test.ts | 30 +++++++++++++++++++ .../src/procedure-utils.test.ts | 10 +++++++ .../tanstack-query/src/procedure-utils.ts | 18 ++++++----- 6 files changed, 102 insertions(+), 14 deletions(-) diff --git a/packages/pinia-colada/src/meta.test.ts b/packages/pinia-colada/src/meta.test.ts index 141b9fa237..17d2dbbe9d 100644 --- a/packages/pinia-colada/src/meta.test.ts +++ b/packages/pinia-colada/src/meta.test.ts @@ -145,6 +145,36 @@ describe('contractOptionsUtilsPlugin', () => { expect(client.planet.find).toHaveBeenCalledTimes(1) }) + it('lets object utils options beat defaults of a function meta modifier', () => { + const contract = { + planet: { + find: oc.meta(piniaColada({ + queryOptions: (options: any) => ({ staleTime: 1000, gcTime: 1, ...options }), + })), + }, + } + + const client = { planet: { find: vi.fn(async () => '__found__') } } + + const utils = createRouterUtils(client as any, { + scoped: { + planet: { + find: { + queryOptions: { gcTime: 2 }, + }, + }, + }, + plugins: [new ContractOptionsUtilsPlugin(contract)], + }) as any + + const options = utils.planet.find.queryOptions({ input: { id: 1 } }) + + expect(options.staleTime).toBe(1000) + expect(options.gcTime).toBe(2) + + expect(utils.planet.find.queryOptions({ input: { id: 1 }, gcTime: 5 }).gcTime).toBe(5) + }) + it('keeps contract meta interceptors when utils define none', async () => { const metaInterceptor = vi.fn(({ next }: any) => next()) const contract = { planet: { find: oc.meta(piniaColada({ queryInterceptors: [metaInterceptor] })) } } diff --git a/packages/pinia-colada/src/procedure-utils.test.ts b/packages/pinia-colada/src/procedure-utils.test.ts index 21297ddba2..1088749e71 100644 --- a/packages/pinia-colada/src/procedure-utils.test.ts +++ b/packages/pinia-colada/src/procedure-utils.test.ts @@ -1004,4 +1004,14 @@ describe('mergeProcedureUtilsOptions', () => { expect((functionFirst.queryOptions as any)({ enabled: true })).toEqual({ enabled: true, staleTime: 1000, gcTime: 2 }) }) + + it('lets an object override beat defaults of a function base, below per-call options', () => { + const merged = mergeProcedureUtilsOptions( + { queryOptions: (options: any) => ({ staleTime: 1000, ...options }) }, + { queryOptions: { staleTime: 0 } }, + ) + + expect((merged.queryOptions as any)({})).toEqual({ staleTime: 0 }) + expect((merged.queryOptions as any)({ staleTime: 5 })).toEqual({ staleTime: 5 }) + }) }) diff --git a/packages/pinia-colada/src/procedure-utils.ts b/packages/pinia-colada/src/procedure-utils.ts index ff01b4e8fc..e850e2b59a 100644 --- a/packages/pinia-colada/src/procedure-utils.ts +++ b/packages/pinia-colada/src/procedure-utils.ts @@ -215,22 +215,26 @@ function mergeProcedureUtilsModifier( base: ProcedureUtilsModifier, override: ProcedureUtilsModifier, ): ProcedureUtilsModifier { - if (typeof base !== 'function' && typeof override !== 'function') { - return { ...base, ...override } as Partial + if (typeof override !== 'function') { + if (typeof base !== 'function') { + return { ...base, ...override } as Partial + } + + return options => base({ ...override, ...options }) } const applyBase = typeof base === 'function' ? base : (options: T) => ({ ...base, ...options }) - const applyOverride = typeof override === 'function' ? override : (options: T) => ({ ...override, ...options }) - return options => applyOverride(applyBase(options)) + return options => override(applyBase(options)) } /** * Merge two procedure utils options where `override` takes priority. * A key explicitly set to `undefined` in `override` resets the base value. - * When both sides define a key: interceptors are concatenated (base ones run first), - * modifiers are spread-merged when both are plain objects and composed (base applied - * first) otherwise, with plain objects applied as regular spread merges. + * When both sides define a key, interceptors are concatenated (base ones run first) and + * modifiers are combined so per-call options beat `override`, which beats `base`: + * two plain objects are spread-merged, an object `override` is passed to a function `base` + * as defaults under the per-call options, and a function `override` receives the result of `base`. */ export function mergeProcedureUtilsOptions( base: ProcedureUtilsOptions, diff --git a/packages/tanstack-query/src/meta.test.ts b/packages/tanstack-query/src/meta.test.ts index 1f244dbbb4..8aa362601a 100644 --- a/packages/tanstack-query/src/meta.test.ts +++ b/packages/tanstack-query/src/meta.test.ts @@ -145,6 +145,36 @@ describe('contractMetaPlugin', () => { expect(client.planet.find).toHaveBeenCalledTimes(1) }) + it('lets object utils options beat defaults of a function meta modifier', () => { + const contract = { + planet: { + find: oc.meta(tanstackQuery({ + queryOptions: (options: any) => ({ staleTime: 1000, retry: 1, ...options }), + })), + }, + } + + const client = { planet: { find: vi.fn(async () => '__found__') } } + + const utils = createRouterUtils(client as any, { + scoped: { + planet: { + find: { + queryOptions: { retry: 2 }, + }, + }, + }, + plugins: [new ContractOptionsUtilsPlugin(contract)], + }) as any + + const options = utils.planet.find.queryOptions({ input: { id: 1 } }) + + expect(options.staleTime).toBe(1000) + expect(options.retry).toBe(2) + + expect(utils.planet.find.queryOptions({ input: { id: 1 }, retry: 5 }).retry).toBe(5) + }) + it('keeps contract meta interceptors when utils define none', async () => { const metaInterceptor = vi.fn(({ next }: any) => next()) const contract = { planet: { find: oc.meta(tanstackQuery({ queryInterceptors: [metaInterceptor] })) } } diff --git a/packages/tanstack-query/src/procedure-utils.test.ts b/packages/tanstack-query/src/procedure-utils.test.ts index 3b7d9a3816..b083fed0c7 100644 --- a/packages/tanstack-query/src/procedure-utils.test.ts +++ b/packages/tanstack-query/src/procedure-utils.test.ts @@ -1206,4 +1206,14 @@ describe('mergeProcedureUtilsOptions', () => { expect((functionFirst.queryOptions as any)({ enabled: true })).toEqual({ enabled: true, staleTime: 1000, retry: 2 }) }) + + it('lets an object override beat defaults of a function base, below per-call options', () => { + const merged = mergeProcedureUtilsOptions( + { queryOptions: (options: any) => ({ staleTime: 1000, ...options }) }, + { queryOptions: { staleTime: 0 } }, + ) + + expect((merged.queryOptions as any)({})).toEqual({ staleTime: 0 }) + expect((merged.queryOptions as any)({ staleTime: 5 })).toEqual({ staleTime: 5 }) + }) }) diff --git a/packages/tanstack-query/src/procedure-utils.ts b/packages/tanstack-query/src/procedure-utils.ts index d817a1b336..ac928d1d0a 100644 --- a/packages/tanstack-query/src/procedure-utils.ts +++ b/packages/tanstack-query/src/procedure-utils.ts @@ -227,22 +227,26 @@ function mergeProcedureUtilsModifier( base: ProcedureUtilsModifier, override: ProcedureUtilsModifier, ): ProcedureUtilsModifier { - if (typeof base !== 'function' && typeof override !== 'function') { - return { ...base, ...override } as Partial + if (typeof override !== 'function') { + if (typeof base !== 'function') { + return { ...base, ...override } as Partial + } + + return options => base({ ...override, ...options }) } const applyBase = typeof base === 'function' ? base : (options: T) => ({ ...base, ...options }) - const applyOverride = typeof override === 'function' ? override : (options: T) => ({ ...override, ...options }) - return options => applyOverride(applyBase(options)) + return options => override(applyBase(options)) } /** * Merge two procedure utils options where `override` takes priority. * A key explicitly set to `undefined` in `override` resets the base value. - * When both sides define a key: interceptors are concatenated (base ones run first), - * modifiers are spread-merged when both are plain objects and composed (base applied - * first) otherwise, with plain objects applied as regular spread merges. + * When both sides define a key, interceptors are concatenated (base ones run first) and + * modifiers are combined so per-call options beat `override`, which beats `base`: + * two plain objects are spread-merged, an object `override` is passed to a function `base` + * as defaults under the per-call options, and a function `override` receives the result of `base`. */ export function mergeProcedureUtilsOptions( base: ProcedureUtilsOptions, From 50fb153cad3d20369ffe3e9107756346e207f6e4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 10:07:42 +0000 Subject: [PATCH 2/5] docs: shorten mergeProcedureUtilsOptions modifier precedence JSDoc State the precedence rule instead of listing each branch of the private merge helper, and note that a function override receives the result of the base. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01B5V7qjA3f5ancGzD2FDQEF --- packages/pinia-colada/src/procedure-utils.ts | 5 ++--- packages/tanstack-query/src/procedure-utils.ts | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/pinia-colada/src/procedure-utils.ts b/packages/pinia-colada/src/procedure-utils.ts index e850e2b59a..38ab5c822a 100644 --- a/packages/pinia-colada/src/procedure-utils.ts +++ b/packages/pinia-colada/src/procedure-utils.ts @@ -232,9 +232,8 @@ function mergeProcedureUtilsModifier( * Merge two procedure utils options where `override` takes priority. * A key explicitly set to `undefined` in `override` resets the base value. * When both sides define a key, interceptors are concatenated (base ones run first) and - * modifiers are combined so per-call options beat `override`, which beats `base`: - * two plain objects are spread-merged, an object `override` is passed to a function `base` - * as defaults under the per-call options, and a function `override` receives the result of `base`. + * modifiers are combined so per-call options beat `override`, which beats `base` + * (a function `override` receives the result of `base`). */ export function mergeProcedureUtilsOptions( base: ProcedureUtilsOptions, diff --git a/packages/tanstack-query/src/procedure-utils.ts b/packages/tanstack-query/src/procedure-utils.ts index ac928d1d0a..ea89326ad7 100644 --- a/packages/tanstack-query/src/procedure-utils.ts +++ b/packages/tanstack-query/src/procedure-utils.ts @@ -244,9 +244,8 @@ function mergeProcedureUtilsModifier( * Merge two procedure utils options where `override` takes priority. * A key explicitly set to `undefined` in `override` resets the base value. * When both sides define a key, interceptors are concatenated (base ones run first) and - * modifiers are combined so per-call options beat `override`, which beats `base`: - * two plain objects are spread-merged, an object `override` is passed to a function `base` - * as defaults under the per-call options, and a function `override` receives the result of `base`. + * modifiers are combined so per-call options beat `override`, which beats `base` + * (a function `override` receives the result of `base`). */ export function mergeProcedureUtilsOptions( base: ProcedureUtilsOptions, From 31e9b77d8455173b46691683d23181a15a27af60 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 07:41:50 +0000 Subject: [PATCH 3/5] docs: clarify that function modifiers win in mergeProcedureUtilsOptions JSDoc The precedence "per-call options beat override, which beats base" only holds for object values: a function modifier receives the merged options and its result wins, even over per-call options. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01B5V7qjA3f5ancGzD2FDQEF --- packages/pinia-colada/src/procedure-utils.ts | 4 ++-- packages/tanstack-query/src/procedure-utils.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/pinia-colada/src/procedure-utils.ts b/packages/pinia-colada/src/procedure-utils.ts index 38ab5c822a..27f8701d08 100644 --- a/packages/pinia-colada/src/procedure-utils.ts +++ b/packages/pinia-colada/src/procedure-utils.ts @@ -232,8 +232,8 @@ function mergeProcedureUtilsModifier( * Merge two procedure utils options where `override` takes priority. * A key explicitly set to `undefined` in `override` resets the base value. * When both sides define a key, interceptors are concatenated (base ones run first) and - * modifiers are combined so per-call options beat `override`, which beats `base` - * (a function `override` receives the result of `base`). + * modifiers are combined so that, for object values, per-call options beat `override`, + * which beats `base`. A function modifier receives the merged options and its result wins. */ export function mergeProcedureUtilsOptions( base: ProcedureUtilsOptions, diff --git a/packages/tanstack-query/src/procedure-utils.ts b/packages/tanstack-query/src/procedure-utils.ts index ea89326ad7..d396574d46 100644 --- a/packages/tanstack-query/src/procedure-utils.ts +++ b/packages/tanstack-query/src/procedure-utils.ts @@ -244,8 +244,8 @@ function mergeProcedureUtilsModifier( * Merge two procedure utils options where `override` takes priority. * A key explicitly set to `undefined` in `override` resets the base value. * When both sides define a key, interceptors are concatenated (base ones run first) and - * modifiers are combined so per-call options beat `override`, which beats `base` - * (a function `override` receives the result of `base`). + * modifiers are combined so that, for object values, per-call options beat `override`, + * which beats `base`. A function modifier receives the merged options and its result wins. */ export function mergeProcedureUtilsOptions( base: ProcedureUtilsOptions, From edf4c0c4893549043be00a854dcc34d7b8043cf9 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 07:55:30 +0000 Subject: [PATCH 4/5] fix: let function overrides beat object base modifiers in query utils With an object base and a function override, the override received the base values merged into its input, so it could not tell them from per-call options and a defaults-style function (`o => ({ x: d, ...o })` or `o.x ?? d`) kept the base value. E.g. an object `queryOptions` in contract meta beat a function `queryOptions` on the utils. The override now receives only the per-call options, and the object base fills the keys missing from its result: per-call options > override > base. Two functions are still composed (base applied first). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01B5V7qjA3f5ancGzD2FDQEF --- packages/pinia-colada/src/meta.test.ts | 30 +++++++++++++++++++ .../pinia-colada/src/procedure-utils.test.ts | 10 +++++++ packages/pinia-colada/src/procedure-utils.ts | 11 ++++--- packages/tanstack-query/src/meta.test.ts | 30 +++++++++++++++++++ .../src/procedure-utils.test.ts | 10 +++++++ .../tanstack-query/src/procedure-utils.ts | 11 ++++--- 6 files changed, 94 insertions(+), 8 deletions(-) diff --git a/packages/pinia-colada/src/meta.test.ts b/packages/pinia-colada/src/meta.test.ts index 17d2dbbe9d..16f675ae04 100644 --- a/packages/pinia-colada/src/meta.test.ts +++ b/packages/pinia-colada/src/meta.test.ts @@ -175,6 +175,36 @@ describe('contractOptionsUtilsPlugin', () => { expect(utils.planet.find.queryOptions({ input: { id: 1 }, gcTime: 5 }).gcTime).toBe(5) }) + it('lets function utils options beat an object meta modifier', () => { + const contract = { + planet: { + find: oc.meta(piniaColada({ + queryOptions: { staleTime: 1000, gcTime: 1 }, + })), + }, + } + + const client = { planet: { find: vi.fn(async () => '__found__') } } + + const utils = createRouterUtils(client as any, { + scoped: { + planet: { + find: { + queryOptions: (options: any) => ({ gcTime: 2, ...options }), + }, + }, + }, + plugins: [new ContractOptionsUtilsPlugin(contract)], + }) as any + + const options = utils.planet.find.queryOptions({ input: { id: 1 } }) + + expect(options.staleTime).toBe(1000) + expect(options.gcTime).toBe(2) + + expect(utils.planet.find.queryOptions({ input: { id: 1 }, gcTime: 5 }).gcTime).toBe(5) + }) + it('keeps contract meta interceptors when utils define none', async () => { const metaInterceptor = vi.fn(({ next }: any) => next()) const contract = { planet: { find: oc.meta(piniaColada({ queryInterceptors: [metaInterceptor] })) } } diff --git a/packages/pinia-colada/src/procedure-utils.test.ts b/packages/pinia-colada/src/procedure-utils.test.ts index 1088749e71..a98a2066c0 100644 --- a/packages/pinia-colada/src/procedure-utils.test.ts +++ b/packages/pinia-colada/src/procedure-utils.test.ts @@ -1014,4 +1014,14 @@ describe('mergeProcedureUtilsOptions', () => { expect((merged.queryOptions as any)({})).toEqual({ staleTime: 0 }) expect((merged.queryOptions as any)({ staleTime: 5 })).toEqual({ staleTime: 5 }) }) + + it('lets a function override beat an object base, which only fills keys missing from its result', () => { + const merged = mergeProcedureUtilsOptions( + { queryOptions: { staleTime: 1000, gcTime: 1 } }, + { queryOptions: (options: any) => ({ gcTime: 2, ...options }) }, + ) + + expect((merged.queryOptions as any)({})).toEqual({ staleTime: 1000, gcTime: 2 }) + expect((merged.queryOptions as any)({ gcTime: 5 })).toEqual({ staleTime: 1000, gcTime: 5 }) + }) }) diff --git a/packages/pinia-colada/src/procedure-utils.ts b/packages/pinia-colada/src/procedure-utils.ts index 27f8701d08..3fbdb37a3a 100644 --- a/packages/pinia-colada/src/procedure-utils.ts +++ b/packages/pinia-colada/src/procedure-utils.ts @@ -223,17 +223,20 @@ function mergeProcedureUtilsModifier( return options => base({ ...override, ...options }) } - const applyBase = typeof base === 'function' ? base : (options: T) => ({ ...base, ...options }) + if (typeof base !== 'function') { + return options => ({ ...base, ...override(options) }) + } - return options => override(applyBase(options)) + return options => override(base(options)) } /** * Merge two procedure utils options where `override` takes priority. * A key explicitly set to `undefined` in `override` resets the base value. * When both sides define a key, interceptors are concatenated (base ones run first) and - * modifiers are combined so that, for object values, per-call options beat `override`, - * which beats `base`. A function modifier receives the merged options and its result wins. + * modifiers are combined so `override` beats `base`, with object values acting as defaults + * under the per-call options. A function modifier's result wins over what it receives, + * and two functions are composed (base applied first). */ export function mergeProcedureUtilsOptions( base: ProcedureUtilsOptions, diff --git a/packages/tanstack-query/src/meta.test.ts b/packages/tanstack-query/src/meta.test.ts index 8aa362601a..be864de1e6 100644 --- a/packages/tanstack-query/src/meta.test.ts +++ b/packages/tanstack-query/src/meta.test.ts @@ -175,6 +175,36 @@ describe('contractMetaPlugin', () => { expect(utils.planet.find.queryOptions({ input: { id: 1 }, retry: 5 }).retry).toBe(5) }) + it('lets function utils options beat an object meta modifier', () => { + const contract = { + planet: { + find: oc.meta(tanstackQuery({ + queryOptions: { staleTime: 1000, retry: 1 }, + })), + }, + } + + const client = { planet: { find: vi.fn(async () => '__found__') } } + + const utils = createRouterUtils(client as any, { + scoped: { + planet: { + find: { + queryOptions: (options: any) => ({ retry: 2, ...options }), + }, + }, + }, + plugins: [new ContractOptionsUtilsPlugin(contract)], + }) as any + + const options = utils.planet.find.queryOptions({ input: { id: 1 } }) + + expect(options.staleTime).toBe(1000) + expect(options.retry).toBe(2) + + expect(utils.planet.find.queryOptions({ input: { id: 1 }, retry: 5 }).retry).toBe(5) + }) + it('keeps contract meta interceptors when utils define none', async () => { const metaInterceptor = vi.fn(({ next }: any) => next()) const contract = { planet: { find: oc.meta(tanstackQuery({ queryInterceptors: [metaInterceptor] })) } } diff --git a/packages/tanstack-query/src/procedure-utils.test.ts b/packages/tanstack-query/src/procedure-utils.test.ts index 730b0904c9..153b8eca78 100644 --- a/packages/tanstack-query/src/procedure-utils.test.ts +++ b/packages/tanstack-query/src/procedure-utils.test.ts @@ -1263,4 +1263,14 @@ describe('mergeProcedureUtilsOptions', () => { expect((merged.queryOptions as any)({})).toEqual({ staleTime: 0 }) expect((merged.queryOptions as any)({ staleTime: 5 })).toEqual({ staleTime: 5 }) }) + + it('lets a function override beat an object base, which only fills keys missing from its result', () => { + const merged = mergeProcedureUtilsOptions( + { queryOptions: { staleTime: 1000, retry: 1 } }, + { queryOptions: (options: any) => ({ retry: 2, ...options }) }, + ) + + expect((merged.queryOptions as any)({})).toEqual({ staleTime: 1000, retry: 2 }) + expect((merged.queryOptions as any)({ retry: 5 })).toEqual({ staleTime: 1000, retry: 5 }) + }) }) diff --git a/packages/tanstack-query/src/procedure-utils.ts b/packages/tanstack-query/src/procedure-utils.ts index d396574d46..e237583ad8 100644 --- a/packages/tanstack-query/src/procedure-utils.ts +++ b/packages/tanstack-query/src/procedure-utils.ts @@ -235,17 +235,20 @@ function mergeProcedureUtilsModifier( return options => base({ ...override, ...options }) } - const applyBase = typeof base === 'function' ? base : (options: T) => ({ ...base, ...options }) + if (typeof base !== 'function') { + return options => ({ ...base, ...override(options) }) + } - return options => override(applyBase(options)) + return options => override(base(options)) } /** * Merge two procedure utils options where `override` takes priority. * A key explicitly set to `undefined` in `override` resets the base value. * When both sides define a key, interceptors are concatenated (base ones run first) and - * modifiers are combined so that, for object values, per-call options beat `override`, - * which beats `base`. A function modifier receives the merged options and its result wins. + * modifiers are combined so `override` beats `base`, with object values acting as defaults + * under the per-call options. A function modifier's result wins over what it receives, + * and two functions are composed (base applied first). */ export function mergeProcedureUtilsOptions( base: ProcedureUtilsOptions, From 3be71cfbd6fc592af158c7a15da0f320fe468024 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 07:58:45 +0000 Subject: [PATCH 5/5] revert: let function overrides see object base values in query utils This reverts commit edf4c0c. A function override modifier again receives the per-call options merged with an object base, so the position of its `...options` spread decides priority: `{ x: d, ...options }` keeps lower layers' values, `{ ...options, x: d }` forces `d`. It also keeps function overrides able to wrap base values such as callbacks. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01B5V7qjA3f5ancGzD2FDQEF --- packages/pinia-colada/src/meta.test.ts | 30 ------------------- .../pinia-colada/src/procedure-utils.test.ts | 10 ------- packages/pinia-colada/src/procedure-utils.ts | 11 +++---- packages/tanstack-query/src/meta.test.ts | 30 ------------------- .../src/procedure-utils.test.ts | 10 ------- .../tanstack-query/src/procedure-utils.ts | 11 +++---- 6 files changed, 8 insertions(+), 94 deletions(-) diff --git a/packages/pinia-colada/src/meta.test.ts b/packages/pinia-colada/src/meta.test.ts index 16f675ae04..17d2dbbe9d 100644 --- a/packages/pinia-colada/src/meta.test.ts +++ b/packages/pinia-colada/src/meta.test.ts @@ -175,36 +175,6 @@ describe('contractOptionsUtilsPlugin', () => { expect(utils.planet.find.queryOptions({ input: { id: 1 }, gcTime: 5 }).gcTime).toBe(5) }) - it('lets function utils options beat an object meta modifier', () => { - const contract = { - planet: { - find: oc.meta(piniaColada({ - queryOptions: { staleTime: 1000, gcTime: 1 }, - })), - }, - } - - const client = { planet: { find: vi.fn(async () => '__found__') } } - - const utils = createRouterUtils(client as any, { - scoped: { - planet: { - find: { - queryOptions: (options: any) => ({ gcTime: 2, ...options }), - }, - }, - }, - plugins: [new ContractOptionsUtilsPlugin(contract)], - }) as any - - const options = utils.planet.find.queryOptions({ input: { id: 1 } }) - - expect(options.staleTime).toBe(1000) - expect(options.gcTime).toBe(2) - - expect(utils.planet.find.queryOptions({ input: { id: 1 }, gcTime: 5 }).gcTime).toBe(5) - }) - it('keeps contract meta interceptors when utils define none', async () => { const metaInterceptor = vi.fn(({ next }: any) => next()) const contract = { planet: { find: oc.meta(piniaColada({ queryInterceptors: [metaInterceptor] })) } } diff --git a/packages/pinia-colada/src/procedure-utils.test.ts b/packages/pinia-colada/src/procedure-utils.test.ts index a98a2066c0..1088749e71 100644 --- a/packages/pinia-colada/src/procedure-utils.test.ts +++ b/packages/pinia-colada/src/procedure-utils.test.ts @@ -1014,14 +1014,4 @@ describe('mergeProcedureUtilsOptions', () => { expect((merged.queryOptions as any)({})).toEqual({ staleTime: 0 }) expect((merged.queryOptions as any)({ staleTime: 5 })).toEqual({ staleTime: 5 }) }) - - it('lets a function override beat an object base, which only fills keys missing from its result', () => { - const merged = mergeProcedureUtilsOptions( - { queryOptions: { staleTime: 1000, gcTime: 1 } }, - { queryOptions: (options: any) => ({ gcTime: 2, ...options }) }, - ) - - expect((merged.queryOptions as any)({})).toEqual({ staleTime: 1000, gcTime: 2 }) - expect((merged.queryOptions as any)({ gcTime: 5 })).toEqual({ staleTime: 1000, gcTime: 5 }) - }) }) diff --git a/packages/pinia-colada/src/procedure-utils.ts b/packages/pinia-colada/src/procedure-utils.ts index 3fbdb37a3a..27f8701d08 100644 --- a/packages/pinia-colada/src/procedure-utils.ts +++ b/packages/pinia-colada/src/procedure-utils.ts @@ -223,20 +223,17 @@ function mergeProcedureUtilsModifier( return options => base({ ...override, ...options }) } - if (typeof base !== 'function') { - return options => ({ ...base, ...override(options) }) - } + const applyBase = typeof base === 'function' ? base : (options: T) => ({ ...base, ...options }) - return options => override(base(options)) + return options => override(applyBase(options)) } /** * Merge two procedure utils options where `override` takes priority. * A key explicitly set to `undefined` in `override` resets the base value. * When both sides define a key, interceptors are concatenated (base ones run first) and - * modifiers are combined so `override` beats `base`, with object values acting as defaults - * under the per-call options. A function modifier's result wins over what it receives, - * and two functions are composed (base applied first). + * modifiers are combined so that, for object values, per-call options beat `override`, + * which beats `base`. A function modifier receives the merged options and its result wins. */ export function mergeProcedureUtilsOptions( base: ProcedureUtilsOptions, diff --git a/packages/tanstack-query/src/meta.test.ts b/packages/tanstack-query/src/meta.test.ts index be864de1e6..8aa362601a 100644 --- a/packages/tanstack-query/src/meta.test.ts +++ b/packages/tanstack-query/src/meta.test.ts @@ -175,36 +175,6 @@ describe('contractMetaPlugin', () => { expect(utils.planet.find.queryOptions({ input: { id: 1 }, retry: 5 }).retry).toBe(5) }) - it('lets function utils options beat an object meta modifier', () => { - const contract = { - planet: { - find: oc.meta(tanstackQuery({ - queryOptions: { staleTime: 1000, retry: 1 }, - })), - }, - } - - const client = { planet: { find: vi.fn(async () => '__found__') } } - - const utils = createRouterUtils(client as any, { - scoped: { - planet: { - find: { - queryOptions: (options: any) => ({ retry: 2, ...options }), - }, - }, - }, - plugins: [new ContractOptionsUtilsPlugin(contract)], - }) as any - - const options = utils.planet.find.queryOptions({ input: { id: 1 } }) - - expect(options.staleTime).toBe(1000) - expect(options.retry).toBe(2) - - expect(utils.planet.find.queryOptions({ input: { id: 1 }, retry: 5 }).retry).toBe(5) - }) - it('keeps contract meta interceptors when utils define none', async () => { const metaInterceptor = vi.fn(({ next }: any) => next()) const contract = { planet: { find: oc.meta(tanstackQuery({ queryInterceptors: [metaInterceptor] })) } } diff --git a/packages/tanstack-query/src/procedure-utils.test.ts b/packages/tanstack-query/src/procedure-utils.test.ts index 153b8eca78..730b0904c9 100644 --- a/packages/tanstack-query/src/procedure-utils.test.ts +++ b/packages/tanstack-query/src/procedure-utils.test.ts @@ -1263,14 +1263,4 @@ describe('mergeProcedureUtilsOptions', () => { expect((merged.queryOptions as any)({})).toEqual({ staleTime: 0 }) expect((merged.queryOptions as any)({ staleTime: 5 })).toEqual({ staleTime: 5 }) }) - - it('lets a function override beat an object base, which only fills keys missing from its result', () => { - const merged = mergeProcedureUtilsOptions( - { queryOptions: { staleTime: 1000, retry: 1 } }, - { queryOptions: (options: any) => ({ retry: 2, ...options }) }, - ) - - expect((merged.queryOptions as any)({})).toEqual({ staleTime: 1000, retry: 2 }) - expect((merged.queryOptions as any)({ retry: 5 })).toEqual({ staleTime: 1000, retry: 5 }) - }) }) diff --git a/packages/tanstack-query/src/procedure-utils.ts b/packages/tanstack-query/src/procedure-utils.ts index e237583ad8..d396574d46 100644 --- a/packages/tanstack-query/src/procedure-utils.ts +++ b/packages/tanstack-query/src/procedure-utils.ts @@ -235,20 +235,17 @@ function mergeProcedureUtilsModifier( return options => base({ ...override, ...options }) } - if (typeof base !== 'function') { - return options => ({ ...base, ...override(options) }) - } + const applyBase = typeof base === 'function' ? base : (options: T) => ({ ...base, ...options }) - return options => override(base(options)) + return options => override(applyBase(options)) } /** * Merge two procedure utils options where `override` takes priority. * A key explicitly set to `undefined` in `override` resets the base value. * When both sides define a key, interceptors are concatenated (base ones run first) and - * modifiers are combined so `override` beats `base`, with object values acting as defaults - * under the per-call options. A function modifier's result wins over what it receives, - * and two functions are composed (base applied first). + * modifiers are combined so that, for object values, per-call options beat `override`, + * which beats `base`. A function modifier receives the merged options and its result wins. */ export function mergeProcedureUtilsOptions( base: ProcedureUtilsOptions,