diff --git a/src/createQueryClient.spec-d.ts b/src/createQueryClient.spec-d.ts index 866e4c5..ca9a1b6 100644 --- a/src/createQueryClient.spec-d.ts +++ b/src/createQueryClient.spec-d.ts @@ -25,22 +25,26 @@ describe('query', () => { test('tags', async () => { const action = () => 'response' - const { query } = createQueryClient() - const numberTag = tag() - const stringTag = tag() + const { query, setQueryData } = createQueryClient() + const stringTag = tag().add() const untypedTag = tag() - // @ts-expect-error - number tag not assignable to string action - query(action, [], { tags: [numberTag, stringTag] }) - - // @ts-expect-error - number tag not assignable to string action - query(action, [], { tags: () => [numberTag, stringTag] }) - query(action, [], { tags: [stringTag, untypedTag] }) query(action, [], { tags: () => [stringTag, untypedTag] }) query(action, [], { tags: [untypedTag] }) query(action, [], { tags: () => [untypedTag] }) + + setQueryData(stringTag, (data) => { + expectTypeOf(data).toEqualTypeOf() + return data + 'bar' + }) + + // @ts-expect-error - sharedTag has data: never, can't return anything useful + setQueryData(untypedTag, (data) => { + expectTypeOf(data).toEqualTypeOf() + return 'could be corrupting' + }) }) }) }) @@ -99,59 +103,63 @@ describe('defineQuery', () => { }) describe('setQueryData', () => { - test('tags', async () => { + test('naked tag is not setQueryData-able (data: never)', () => { + const { setQueryData } = createQueryClient() + const myTag = tag() + + // @ts-expect-error - data: never, return must be never (effectively impossible) + setQueryData(myTag, () => 'anything') + }) + + test('typed tag passes data through with its declared type', () => { const { setQueryData } = createQueryClient() const numberTag = tag() - const stringTag = tag() - const untypedTag = tag() - setQueryData(untypedTag, (data) => { - expectTypeOf(data).toEqualTypeOf() - return 'foo' + setQueryData(numberTag, (data) => { + expectTypeOf(data).toEqualTypeOf() + return data + 1 }) + // @ts-expect-error - returning wrong type setQueryData(numberTag, (data) => { expectTypeOf(data).toEqualTypeOf() - return 2 + return 'wrong type' }) + }) - setQueryData(stringTag, (data) => { - expectTypeOf(data).toEqualTypeOf() - return 'new string' - }) + test('descendant tag has its own data type', () => { + const { setQueryData } = createQueryClient() + const sharedTag = tag() + const userTag = sharedTag.add<{ id: number }>() - setQueryData([untypedTag], (data) => { - expectTypeOf(data).toEqualTypeOf() - return 'foo' + setQueryData(userTag, (data) => { + expectTypeOf(data).toEqualTypeOf<{ id: number }>() + return data }) - setQueryData([numberTag], (data) => { - expectTypeOf(data).toEqualTypeOf() - return 2 - }) + // ancestor tag is still locked at the type level + // @ts-expect-error - sharedTag has data: never + setQueryData(sharedTag, (data) => { + expectTypeOf(data).toEqualTypeOf() - // this is kinda interesting, no matter the data the return type is the union :thinking: - // so there's not really a type safe way to update multiple queries at once - setQueryData([numberTag, stringTag], (data) => { - expectTypeOf(data).toEqualTypeOf() - return 'foo' + return 'could be corrupting' }) + }) - setQueryData([untypedTag, stringTag, numberTag], (data) => { - expectTypeOf(data).toEqualTypeOf() - return 'foo' - }) + test('descendants nest arbitrarily deep', () => { + const { setQueryData } = createQueryClient() + const sharedTag = tag() + const userTag = sharedTag.add<{ id: number }>() + const userAvatarTag = userTag.add<{ id: number, url: string }>() - // @ts-expect-error - number tag not assignable to string action - setQueryData(numberTag, (data) => { - expectTypeOf(data).toEqualTypeOf() - return 'string' + setQueryData(userAvatarTag, (data) => { + expectTypeOf(data).toEqualTypeOf<{ id: number, url: string }>() + return data }) - // @ts-expect-error - number tag not assignable to string action - setQueryData([numberTag, stringTag], (data) => { - expectTypeOf(data).toEqualTypeOf() - return [] + setQueryData(userTag, (data) => { + expectTypeOf(data).toEqualTypeOf<{ id: number }>() + return data }) }) @@ -252,12 +260,14 @@ describe('refreshQueryData', () => { test('tags', () => { const { refreshQueryData } = createQueryClient() - const numberTag = tag() - const stringTag = tag() + const sharedTag = tag() + const numberTag = sharedTag.add() + const stringTag = sharedTag.add() const action = (param: number) => param + refreshQueryData(sharedTag) refreshQueryData(numberTag) - refreshQueryData([numberTag, stringTag]) + refreshQueryData(stringTag) refreshQueryData(action) refreshQueryData(action, [2]) diff --git a/src/createQueryClient.spec.ts b/src/createQueryClient.spec.ts index 484ff8c..9d2772a 100644 --- a/src/createQueryClient.spec.ts +++ b/src/createQueryClient.spec.ts @@ -562,22 +562,18 @@ describe('setQueryData', () => { await vi.runOnlyPendingTimersAsync() - setQueryData(stringTag, () => { - return 'bar' - }) - - setQueryData(numberTag, () => { - return 2 - }) + setQueryData(stringTag, () => 'bar') + setQueryData(numberTag, () => 2) expect(stringQuery.data).toBe('bar') expect(numberQuery.data).toBe(2) }) - test('tags', async () => { + test('descendant tag setter only matches that descendant\'s queries', async () => { const { setQueryData, query } = createQueryClient() - const stringTag = tag() - const numberTag = tag() + const sharedTag = tag() + const stringTag = sharedTag.add() + const numberTag = sharedTag.add() const stringAction = () => 'foo' const numberAction = () => 1 @@ -587,16 +583,10 @@ describe('setQueryData', () => { await vi.runOnlyPendingTimersAsync() - setQueryData([stringTag], () => { - return 'bar' - }) + setQueryData(stringTag, (data) => data + '-bar') - setQueryData([numberTag], () => { - return 2 - }) - - expect(stringQuery.data).toBe('bar') - expect(numberQuery.data).toBe(2) + expect(stringQuery.data).toBe('foo-bar') + expect(numberQuery.data).toBe(1) }) test('action', async () => { @@ -656,8 +646,8 @@ describe('refreshQueryData', () => { const numberAction = vi.fn() const stringAction = vi.fn() - const numberTag = tag() - const stringTag = tag() + const numberTag = tag() + const stringTag = tag() query(numberAction, [], { tags: [numberTag] }) query(stringAction, [], { tags: [stringTag] }) @@ -795,7 +785,7 @@ describe('mutate', () => { [undefined], ])('refreshes tagged queries: %s', async (refreshQueryData) => { const { mutate, query } = createQueryClient() - const numberTag = tag() + const numberTag = tag() const queryAction = vi.fn() const mutationAction = vi.fn() @@ -817,7 +807,7 @@ describe('mutate', () => { test('does not refresh tagged queries if refreshQueryData is false', async () => { const { mutate, query } = createQueryClient() - const numberTag = tag() + const numberTag = tag() const queryAction = vi.fn() const mutationAction = vi.fn() @@ -839,7 +829,7 @@ describe('mutate', () => { test('does not refresh tagged queries if the action throws an error', async () => { const { mutate, query } = createQueryClient() - const numberTag = tag() + const numberTag = tag() const queryAction = vi.fn() const mutationAction = vi.fn(() => { throw new Error() @@ -1047,7 +1037,7 @@ describe('useMutation', () => { test('setQueryDataBefore and setQueryDataAfter are called when the mutation is executed', async () => { const { useMutation, query } = createQueryClient() const { promise, resolve } = Promise.withResolvers() - const numberTag = tag() + const numberTag = tag() const queryAction = vi.fn() const mutationAction = vi.fn(() => promise) const setQueryDataBefore = vi.fn() @@ -1306,8 +1296,8 @@ describe('defineMutation', () => { const { promise, resolve } = Promise.withResolvers() const mutationPayload = 1 const mutationAction = (value: number) => promise.then(() => value) - const tagA = tag() - const tagB = tag() + const tagA = tag() + const tagB = tag() const queryAResponse = 1 const queryBResponse = 1 const queryAAction = () => queryAResponse @@ -1627,8 +1617,8 @@ describe('defineMutation', () => { const { promise, resolve } = Promise.withResolvers() const mutationPayload = 1 const mutationAction = (value: number) => promise.then(() => value) - const tagA = tag() - const tagB = tag() + const tagA = tag() + const tagB = tag() const queryAResponse = 1 const queryBResponse = 1 const queryAAction = () => queryAResponse diff --git a/src/createQueryClient.ts b/src/createQueryClient.ts index 855a6f5..9a653de 100644 --- a/src/createQueryClient.ts +++ b/src/createQueryClient.ts @@ -13,7 +13,7 @@ import { } from './types/client' import { createQueryGroups } from './createQueryGroups' import { createUseQuery } from './createUseQuery' -import { isQueryTag, isQueryTags, QueryTag } from './types/tags' +import { isQueryTag, QueryTag } from './types/tags' import { isArray } from './utilities/arrays' import { assertNever } from './utilities/assert' import { QueryGroup } from './createQueryGroup' @@ -59,7 +59,7 @@ export function createQueryClient(options?: ClientOptions): QueryClient { } const setQueryData: SetQueryData = ( - param1: QueryTag | QueryTag[] | QueryAction, + param1: QueryTag | QueryAction, param2: Parameters | QueryDataSetter, param3?: QueryDataSetter, ): void => { @@ -72,10 +72,10 @@ export function createQueryClient(options?: ClientOptions): QueryClient { }) } - if (isQueryTag(param1) || isQueryTags(param1)) { - const tags = param1 + if (isQueryTag(param1)) { + const queryTag = param1 const setter = param2 as QueryDataSetter - const groups = getQueryGroups(tags) + const groups = getQueryGroups(queryTag) setDataForGroups(groups, setter) @@ -107,12 +107,12 @@ export function createQueryClient(options?: ClientOptions): QueryClient { } const refreshQueryData: RefreshQueryData = ( - param1: QueryTag | QueryTag[] | QueryAction, + param1: QueryTag | QueryAction, param2?: Parameters, ): void => { - if (isQueryTag(param1) || isQueryTags(param1)) { - const tags = param1 - const groups = getQueryGroups(tags) + if (isQueryTag(param1)) { + const queryTag = param1 + const groups = getQueryGroups(queryTag) groups.forEach((group) => { group.execute() @@ -133,7 +133,7 @@ export function createQueryClient(options?: ClientOptions): QueryClient { return } - assertNever(param1, 'Invalid arguments given to setQueryData') + assertNever(param1, 'Invalid arguments given to refreshQueryData') } const mutate: MutationFunction = (action, parameters, options) => { diff --git a/src/createQueryGroup.spec.ts b/src/createQueryGroup.spec.ts index 25144b0..915c296 100644 --- a/src/createQueryGroup.spec.ts +++ b/src/createQueryGroup.spec.ts @@ -244,31 +244,27 @@ describe('given group with tags', () => { test('can check if it has a tag', async () => { const group = createQueryGroup(vi.fn(), []) const tag1 = tag() - const tag2 = tag((value: string) => value) + const tag2 = tag() expect(group.hasTag(tag1)).toBe(false) const query1 = group.createQuery({ tags: [tag1] }) - const query2 = group.createQuery({ tags: [tag2('foo')] }) + const query2 = group.createQuery({ tags: [tag2] }) - // need executed to happen for tag factories await vi.advanceTimersByTimeAsync(0) expect(group.hasTag(tag1)).toBe(true) - expect(group.hasTag(tag2('foo'))).toBe(true) - expect(group.hasTag(tag2('bar'))).toBe(false) + expect(group.hasTag(tag2)).toBe(true) query1.dispose() expect(group.hasTag(tag1)).toBe(false) - expect(group.hasTag(tag2('foo'))).toBe(true) - expect(group.hasTag(tag2('bar'))).toBe(false) + expect(group.hasTag(tag2)).toBe(true) query2.dispose() expect(group.hasTag(tag1)).toBe(false) - expect(group.hasTag(tag2('foo'))).toBe(false) - expect(group.hasTag(tag2('bar'))).toBe(false) + expect(group.hasTag(tag2)).toBe(false) }) }) diff --git a/src/createQueryGroupTags.ts b/src/createQueryGroupTags.ts index 1875e2a..8ff791c 100644 --- a/src/createQueryGroupTags.ts +++ b/src/createQueryGroupTags.ts @@ -2,52 +2,55 @@ import { QueryTag } from './types/tags' import { TagKey } from './getTagKey' export function createQueryGroupTags() { - const tags = new Map>() - const queries = new Map>() + const tagKeyToQueryIds = new Map>() + const queryIdToTags = new Map>() function clear(): void { - tags.clear() - queries.clear() + tagKeyToQueryIds.clear() + queryIdToTags.clear() } function has(tag: QueryTag): boolean { - return tags.has(tag.key) + return tagKeyToQueryIds.has(tag.key) } - function getQueryIdsByTag(tag: QueryTag): Set { - if (!tags.has(tag.key)) { - tags.set(tag.key, new Set()) + function getQueryIdsForKey(key: TagKey): Set { + if (!tagKeyToQueryIds.has(key)) { + tagKeyToQueryIds.set(key, new Set()) } - return tags.get(tag.key)! + return tagKeyToQueryIds.get(key)! } function getTagsByQueryId(queryId: number): Set { - if (!queries.has(queryId)) { - queries.set(queryId, new Set()) + if (!queryIdToTags.has(queryId)) { + queryIdToTags.set(queryId, new Set()) } - return queries.get(queryId)! + return queryIdToTags.get(queryId)! } function addTag(tag: QueryTag, queryId: number): void { - getQueryIdsByTag(tag).add(queryId) + for (const key of tag.keys) { + getQueryIdsForKey(key).add(queryId) + } + getTagsByQueryId(queryId).add(tag) } function removeTag(tag: QueryTag, queryId: number): void { - const queryTags = getQueryIdsByTag(tag) - const tagQueries = getTagsByQueryId(queryId) - - queryTags.delete(queryId) - tagQueries.delete(tag) - - if (queryTags.size === 0) { - tags.delete(tag.key) + for (const key of tag.keys) { + const queryIds = getQueryIdsForKey(key) + queryIds.delete(queryId) + if (queryIds.size === 0) { + tagKeyToQueryIds.delete(key) + } } - if (tagQueries.size === 0) { - queries.delete(queryId) + const tagSet = getTagsByQueryId(queryId) + tagSet.delete(tag) + if (tagSet.size === 0) { + queryIdToTags.delete(queryId) } } diff --git a/src/createUseQuery.ts b/src/createUseQuery.ts index 17e9878..6cc39ba 100644 --- a/src/createUseQuery.ts +++ b/src/createUseQuery.ts @@ -1,7 +1,7 @@ import { CreateQuery } from './createQueryGroups' import { Query, QueryAction, QueryActionArgs } from './types/query' import { onScopeDispose, ref, toRef, toRefs, toValue, watch } from 'vue' -import equal from "fast-deep-equal" +import equal from 'fast-deep-equal' import { isDefined } from './utilities' import { UseQueryOptions } from './types/client' @@ -23,7 +23,7 @@ export function createUseQuery(createQuery: CreateQuery, action: QueryAction, pa watch(() => ({ enabled: enabled.value, parameters: toValue(parameters) }) as const, ({ enabled, parameters }, previous) => { const isSameParameters = previous && isDefined(previous.parameters) && equal(previous.parameters, parameters) - const isSameEnabled = previous && previous.enabled === enabled + const isSameEnabled = previous?.enabled === enabled if (isSameParameters && isSameEnabled) { return diff --git a/src/getAllTags.spec.ts b/src/getAllTags.spec.ts index b932cf4..76e14c4 100644 --- a/src/getAllTags.spec.ts +++ b/src/getAllTags.spec.ts @@ -4,18 +4,18 @@ import { tag } from './tag' const tagA = tag() const tagB = tag() -const tagC = tag((input: string) => input) +const tagC = tag() test('given tags returns all tags', () => { - const tags = getAllTags([tagA, tagB, tagC('foo')], undefined) + const tags = getAllTags([tagA, tagB, tagC], undefined) - expect(tags).toEqual([tagA, tagB, tagC('foo')]) + expect(tags).toEqual([tagA, tagB, tagC]) }) test('given a function returns all tags', () => { - const tags = getAllTags((input: string) => [tagA, tagB, tagC(input)], 'foo') + const tags = getAllTags(() => [tagA, tagB, tagC], 'foo') - expect(tags).toEqual([tagA, tagB, tagC('foo')]) + expect(tags).toEqual([tagA, tagB, tagC]) }) test('given no tags returns an empty array', () => { diff --git a/src/tag.spec-d.ts b/src/tag.spec-d.ts index 312f569..f1b783b 100644 --- a/src/tag.spec-d.ts +++ b/src/tag.spec-d.ts @@ -1,38 +1,60 @@ import { expectTypeOf, test, vi } from 'vitest' -import { QueryTag, QueryTagFactory, Unset } from '@/types/tags' +import { QueryTag } from '@/types/tags' import { createQueryClient } from './createQueryClient' import { tag } from './tag' -test('tag function returns a tag when no callback is provided', () => { +test('tag function returns a QueryTag', () => { const value = tag() expectTypeOf(value).toExtend() + expectTypeOf(value).toEqualTypeOf>() }) -test('tag function returns a tag factory when a callback is provided', () => { - const factory = tag((string: string) => string) +test('tag() returns a typed QueryTag', () => { + const value = tag() - expectTypeOf(factory).toExtend>() + expectTypeOf(value).toEqualTypeOf>() +}) + +test('tag.add() returns a typed descendant', () => { + const baseTag = tag() + const userTag = baseTag.add<{ id: number }>() + + expectTypeOf(userTag).toEqualTypeOf>() +}) - const value = factory('foo') +test('descendants nest arbitrarily deep', () => { + const baseTag = tag() + const userTag = baseTag.add<{ id: number }>() + const userAvatarTag = userTag.add<{ id: number, url: string }>() - expectTypeOf(value).toEqualTypeOf>() + expectTypeOf(userAvatarTag).toEqualTypeOf>() }) -test('tag function returns a typed tag when data generic is provided', () => { - const value = tag() +test('descendant data must be assignable to ancestor data', () => { + type User = { id: number } + type UserImage = { id: number, url: string } + type UserDetails = { id: number, bio: string } + + const usersTag = tag() - expectTypeOf(value).toEqualTypeOf>() + expectTypeOf(usersTag.add()).toEqualTypeOf>() + expectTypeOf(usersTag.add()).toEqualTypeOf>() + expectTypeOf(usersTag.add()).toEqualTypeOf>() }) -test('tag factory returns a typed tag when data generic is provided', () => { - const factory = tag((value: string) => value) +test('descendant with data not assignable to ancestor is rejected', () => { + const userTag = tag<{ id: number }>() - expectTypeOf(factory).toEqualTypeOf>() + // @ts-expect-error - { unrelated: true } is not assignable to { id: number } + userTag.add<{ unrelated: true }>() +}) - const value = factory('foo') +test('untyped root places no constraint on descendants', () => { + const baseTag = tag() - expectTypeOf(value).toEqualTypeOf>() + expectTypeOf(baseTag.add()).toEqualTypeOf>() + expectTypeOf(baseTag.add<{ anything: true }>()).toEqualTypeOf>() }) test('query from query function with tags callback is called with the query data', () => { diff --git a/src/tag.spec.ts b/src/tag.spec.ts index e8d9ac9..f345056 100644 --- a/src/tag.spec.ts +++ b/src/tag.spec.ts @@ -8,19 +8,28 @@ test('tags are unique', () => { expect(tag1).not.toBe(tag2) }) -test('tag factories are unique', () => { - const factory1 = tag((string: string) => string) - const factory2 = tag((string: string) => string) - const value1 = factory1('foo') - const value2 = factory2('foo') +test('tag.add returns a new descendant tag', () => { + const baseTag = tag() + const userTag = baseTag.add() - expect(value1).not.toBe(value2) + expect(userTag).toBeDefined() + expect(userTag.key).not.toBe(baseTag.key) }) -test('tag factory returns the same key when given the same value', () => { - const factory = tag((string: string) => string) - const value1 = factory('foo') - const value2 = factory('foo') +test('descendants carry their ancestor keys', () => { + const baseTag = tag() + const userTag = baseTag.add() - expect(value1.key).toBe(value2.key) + expect(userTag.keys).toContain(baseTag.key) + expect(userTag.keys).toContain(userTag.key) +}) + +test('chained .add calls accumulate ancestor keys', () => { + const baseTag = tag() + const userTag = baseTag.add<{ id: number }>() + const deepTag = userTag.add<{ id: number, label: string }>() + + expect(deepTag.keys).toContain(baseTag.key) + expect(deepTag.keys).toContain(userTag.key) + expect(deepTag.keys).toContain(deepTag.key) }) diff --git a/src/tag.ts b/src/tag.ts index 1acb2d9..49c4f9d 100644 --- a/src/tag.ts +++ b/src/tag.ts @@ -1,24 +1,28 @@ import { createSequence } from './createSequence' -import { getTagKey } from './getTagKey' -import { QueryTagFactory, QueryTagCallback, QueryTag, Unset, unset } from './types/tags' +import { getTagKey, TagKey } from './getTagKey' +import { QueryTag, unset } from './types/tags' const createTagId = createSequence() -function createQueryTag(id: number, value: unknown): QueryTag { - return { - data: unset, - key: getTagKey(id, value), - } -} - -export function tag(): QueryTag -export function tag(callback: QueryTagCallback): QueryTagFactory -export function tag(callback?: QueryTagCallback): QueryTag | QueryTagFactory { +function createTag(parentKeys: readonly TagKey[]): QueryTag { const id = createTagId() + const ownKey = getTagKey(id, undefined) + const keys = Object.freeze([...parentKeys, ownKey]) - if (callback) { - return (value) => createQueryTag(id, callback(value)) + const queryTag = { + data: unset, + key: ownKey, + keys, + add(): QueryTag { + return createTag(keys) + }, } - return createQueryTag(id, undefined) + return queryTag as unknown as QueryTag +} + +export function tag(): QueryTag +export function tag(): QueryTag +export function tag(): QueryTag { + return createTag([]) } diff --git a/src/types/client.ts b/src/types/client.ts index 768e885..1738ba1 100644 --- a/src/types/client.ts +++ b/src/types/client.ts @@ -63,13 +63,13 @@ export type DefinedQuery< export type QueryDataSetter = (data: T) => T export type SetQueryData = { - (tag: TQueryTag | TQueryTag[], setter: QueryDataSetter>): void, + (tag: TQueryTag, setter: QueryDataSetter>): void, (action: TAction, setter: QueryDataSetter>): void, (action: TAction, parameters: Parameters, setter: QueryDataSetter>): void, } export type RefreshQueryData = { - (tag: TQueryTag | TQueryTag[]): void, + (tag: QueryTag): void, (action: QueryAction): void, (action: TAction, parameters: Parameters): void, } diff --git a/src/types/query.ts b/src/types/query.ts index a3a5cb7..0368279 100644 --- a/src/types/query.ts +++ b/src/types/query.ts @@ -1,6 +1,6 @@ import { RetryOptions } from '@/utilities/retry' import { Getter } from './getters' -import { QueryTag, Unset } from '@/types/tags' +import { QueryTag } from '@/types/tags' import { DefaultValue } from './utilities' export type QueryAction = (...args: any[]) => any @@ -19,7 +19,7 @@ export type QueryActionArgs< export type QueryTags< TAction extends QueryAction = QueryAction -> = QueryTag | Unset>[] | ((value: QueryData) => QueryTag | Unset>[]) +> = QueryTag>[] | ((value: QueryData) => QueryTag>[]) export type QueryOptions< TAction extends QueryAction = QueryAction, diff --git a/src/types/tags.ts b/src/types/tags.ts index 892fd30..f7c2f77 100644 --- a/src/types/tags.ts +++ b/src/types/tags.ts @@ -3,26 +3,40 @@ import { TagKey } from '@/getTagKey' export const unset = Symbol('unset') export type Unset = typeof unset -export type QueryTag< - TData = unknown -> = { +export type QueryTag = { /** * @private * @internal - * This property is unused, but necessary to preserve the type for TData because unused generics are ignored by typescript. + * Phantom field used purely to preserve the TData generic in the type; + * the runtime value is always the `unset` symbol regardless of TData. */ data: TData, + /** + * The tag's own unique key. + */ key: TagKey, + /** + * Own key plus all ancestor keys, in root-to-leaf order. A query tagged + * with this tag is registered against every key in this list, so + * setQueryData / invalidateQueries on any ancestor matches the query. + */ + keys: readonly TagKey[], + /** + * Create a typed descendant of this tag. The descendant inherits this + * tag's identity, so any operation against this tag also matches queries + * tagged with the descendant. The descendant's data type must be assignable + * to this tag's data type, so the parent acts as a supertype of all + * descendants. An untyped root (`tag()`) places no constraint on descendants. + */ + add: () => QueryTag, } export type QueryTagType = TQueryTag extends QueryTag - ? TData extends Unset - ? unknown - : TData + ? TData : never export function isQueryTag(tag: unknown): tag is QueryTag { - return typeof tag === 'object' && tag !== null && 'data' in tag && 'key' in tag + return typeof tag === 'object' && tag !== null && 'data' in tag && 'key' in tag && 'keys' in tag } export function isQueryTags(tags: unknown): tags is QueryTag[] { diff --git a/src/types/utilities.ts b/src/types/utilities.ts index 24e26ad..3d14cf5 100644 --- a/src/types/utilities.ts +++ b/src/types/utilities.ts @@ -1 +1,4 @@ export type DefaultValue = unknown extends TValue ? TDefault : TValue + +export type UnionToIntersection = + (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never diff --git a/src/utilities/createDefinedMutationOptions.ts b/src/utilities/createDefinedMutationOptions.ts index 047ecea..a435b70 100644 --- a/src/utilities/createDefinedMutationOptions.ts +++ b/src/utilities/createDefinedMutationOptions.ts @@ -47,14 +47,18 @@ export function createDefinedMutationOptions({ const tags = getAllTags(options?.tags, undefined) const setter = (data: QueryData) => setQueryDataBefore(data, context) - setQueryData(tags, setter) + for (const tag of tags) { + setQueryData(tag, setter) + } } if (definedSetQueryDataBefore) { const tags = getAllTags(definedOptions?.tags, undefined) const setter = (data: QueryData) => definedSetQueryDataBefore(data, context) - setQueryData(tags, setter) + for (const tag of tags) { + setQueryData(tag, setter) + } } onExecute?.(context) @@ -66,16 +70,26 @@ export function createDefinedMutationOptions({ const definedTags = getAllTags(definedOptions?.tags, context.data) if (shouldRefreshQueryData) { - refreshQueryData(tags) - refreshQueryData(definedTags) + for (const tag of tags) { + refreshQueryData(tag) + } + for (const tag of definedTags) { + refreshQueryData(tag) + } } if (setQueryDataAfter) { - setQueryData(tags, (queryData: QueryData): QueryData => setQueryDataAfter(queryData, context)) + const setter = (queryData: QueryData): QueryData => setQueryDataAfter(queryData, context) + for (const tag of tags) { + setQueryData(tag, setter) + } } if (definedSetQueryDataAfter) { - setQueryData(definedTags, (queryData: QueryData): QueryData => definedSetQueryDataAfter(queryData, context)) + const setter = (queryData: QueryData): QueryData => definedSetQueryDataAfter(queryData, context) + for (const tag of definedTags) { + setQueryData(tag, setter) + } } onSuccess?.(context) diff --git a/src/utilities/createMutationOptions.ts b/src/utilities/createMutationOptions.ts index e7e726b..27d7ab2 100644 --- a/src/utilities/createMutationOptions.ts +++ b/src/utilities/createMutationOptions.ts @@ -26,7 +26,10 @@ export function createMutationOptions({ options, setQueryData, refreshQueryData payload: context.payload, } satisfies MutationTagsBeforeContext) - setQueryData(tags, (queryData: QueryData): QueryData => setQueryDataBefore(queryData, context)) + const setter = (queryData: QueryData): QueryData => setQueryDataBefore(queryData, context) + for (const tag of tags) { + setQueryData(tag, setter) + } } onExecute?.(context) @@ -39,11 +42,16 @@ export function createMutationOptions({ options, setQueryData, refreshQueryData } satisfies MutationTagsAfterContext) if (options?.refreshQueryData ?? true) { - refreshQueryData(tags) + for (const tag of tags) { + refreshQueryData(tag) + } } if (setQueryDataAfter) { - setQueryData(tags, (queryData: QueryData): QueryData => setQueryDataAfter(queryData, context)) + const setter = (queryData: QueryData): QueryData => setQueryDataAfter(queryData, context) + for (const tag of tags) { + setQueryData(tag, setter) + } } onSuccess?.(context)