Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .changeset/fluffy-papayas-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@genseki/react-query": patch
"@genseki/react": patch
---

Fix query key in useInvalidateQueries
3 changes: 2 additions & 1 deletion packages/react-query/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"format": "prettier --write .",
"format:check": "prettier --check .",
"typecheck": "tsc",
"bundle": "rm -rf dist && tsc -p tsconfig.bundle.json"
"bundle": "rm -rf dist && tsc -p tsconfig.bundle.json",
"test": "vitest run"
},
"keywords": [],
"author": "",
Expand Down
23 changes: 18 additions & 5 deletions packages/react-query/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import type {
} from '@genseki/react'
import { createRestClient, type CreateRestClientConfig } from '@genseki/rest'

import { isEmptyObject, normalizeObject, sortObjectDeep } from './utils'

type QueryMethod = 'GET'
type MutationMethod = 'POST' | 'PATCH' | 'PUT' | 'DELETE'
type Method = QueryMethod | MutationMethod
Expand Down Expand Up @@ -210,12 +212,23 @@ export type QueryClient<TApiRouter extends FlatApiRouter> =
: never

export function queryKey(method: string, path: string | number | symbol, payload?: any) {
const payloadKey = {
pathParams: payload?.pathParams ?? {},
query: payload?.query ?? {},
headers: payload?.headers ?? {},
const { header, ...rest } = payload
const normalizedPayload = normalizeObject(rest)
const normalizedHeaders = header && !isEmptyObject(header) ? sortObjectDeep(header) : undefined

if (normalizedHeaders && normalizedPayload) {
return [method, path, normalizedPayload, normalizedHeaders] as const
}

if (normalizedHeaders) {
return [method, path, normalizedHeaders] as const
}
return [method, path, payloadKey] as const

if (normalizedPayload) {
return [method, path, normalizedPayload] as const
}

return [method, path] as const
}

export function createQueryClient<TApp extends GensekiAppCompiled>(
Expand Down
154 changes: 154 additions & 0 deletions packages/react-query/src/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { describe, expect, it } from 'vitest'

import { isEmptyObject, normalizeObject, sortObjectDeep } from './utils'

describe('isEmptyObject', () => {
it('should return true for empty object', () => {
expect(isEmptyObject({})).toBe(true)
})

it('should return false for object with properties', () => {
expect(isEmptyObject({ id: 1 })).toBe(false)
})

it('should return false for null', () => {
expect(isEmptyObject(null)).toBe(false)
})

it('should return false for undefined', () => {
expect(isEmptyObject(undefined)).toBe(false)
})

it('should return false for arrays', () => {
expect(isEmptyObject([])).toBe(false)
expect(isEmptyObject([1, 2, 3])).toBe(false)
})

it('should return false for primitives', () => {
expect(isEmptyObject('string')).toBe(false)
expect(isEmptyObject(42)).toBe(false)
expect(isEmptyObject(true)).toBe(false)
})
})

describe('sortObjectDeep', () => {
it('should sort object keys alphabetically', () => {
const input = { z: 1, a: 2, m: 3 }
const expected = { a: 2, m: 3, z: 1 }
expect(sortObjectDeep(input)).toEqual(expected)
})

it('should sort nested objects recursively', () => {
const input = {
z: { y: 1, x: 2 },
a: { c: 3, b: 4 },
}
const expected = {
a: { b: 4, c: 3 },
z: { x: 2, y: 1 },
}
expect(sortObjectDeep(input)).toEqual(expected)
})

it('should handle arrays without sorting', () => {
const input = { items: [3, 1, 2] }
const expected = { items: [3, 1, 2] }
expect(sortObjectDeep(input)).toEqual(expected)
})

it('should handle primitives', () => {
expect(sortObjectDeep(null)).toBe(null)
expect(sortObjectDeep(undefined)).toBe(undefined)
expect(sortObjectDeep(42)).toBe(42)
expect(sortObjectDeep('string')).toBe('string')
})

it('should handle deeply nested mixed structures', () => {
const input = {
z: { nested: { b: 1, a: 2 } },
a: [1, 2, 3],
m: 'string',
}
const expected = {
a: [1, 2, 3],
m: 'string',
z: { nested: { a: 2, b: 1 } },
}
expect(sortObjectDeep(input)).toEqual(expected)
})

it('should maintain object references for primitives', () => {
const obj = { value: 42 }
expect(sortObjectDeep(obj)).toEqual({ value: 42 })
})
})

describe('normalizeObject', () => {
it('should return undefined for falsy inputs', () => {
expect(normalizeObject()).toBe(undefined)
expect(normalizeObject(null)).toBe(undefined)
expect(normalizeObject(undefined)).toBe(undefined)
expect(normalizeObject(false)).toBe(undefined)
})

it('should return undefined when all fields are empty', () => {
const payload = {
query: {},
pathParams: {},
}
expect(normalizeObject(payload)).toBe(undefined)
})

it('should filter and sort non-empty objects', () => {
const payload = {
query: { z: 1, a: 2 },
pathParams: { id: '123' },
}
const expected = {
pathParams: { id: '123' },
query: { a: 2, z: 1 },
}
expect(normalizeObject(payload)).toEqual(expected)
})

it('should exclude empty objects while keeping non-empty ones', () => {
const payload = {
query: { name: 'John' },
pathParams: {},
}
const expected = {
query: { name: 'John' },
}
expect(normalizeObject(payload)).toEqual(expected)
})

it('should handle nested objects in query and pathParams', () => {
const payload = {
query: {
filter: { z: 'last', a: 'first' },
sort: 'name',
},
pathParams: {
nested: { b: 2, a: 1 },
},
}
const expected = {
pathParams: {
nested: { a: 1, b: 2 },
},
query: {
filter: { a: 'first', z: 'last' },
sort: 'name',
},
}
expect(normalizeObject(payload)).toEqual(expected)
})

it('should handle null and undefined values in fields', () => {
const payload = {
query: null,
pathParams: undefined,
}
expect(normalizeObject(payload)).toBe(undefined)
})
})
31 changes: 31 additions & 0 deletions packages/react-query/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export function isEmptyObject(obj: any): boolean {
return (
obj != null && typeof obj === 'object' && !Array.isArray(obj) && Object.keys(obj).length === 0
)
}

export function sortObjectDeep(obj: any): any {
if (obj == null || typeof obj !== 'object' || Array.isArray(obj)) {
return obj
}

return Object.keys(obj)
.sort()
.reduce((result, key) => {
result[key] = sortObjectDeep(obj[key])
return result
}, {} as any)
}

export function normalizeObject(payload?: any): any {
if (!payload) return undefined

const filtered = Object.entries(payload).reduce((acc, [key, value]) => {
if (value != null && !isEmptyObject(value)) {
acc[key] = sortObjectDeep(value)
}
return acc
}, {} as any)

return Object.keys(filtered).length > 0 ? filtered : undefined
}
5 changes: 2 additions & 3 deletions packages/react/src/react/views/collections/list/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,9 @@ function _CollectionListProvider<T extends BaseData>(props: CollectionListProvid
const queryClient = useQueryClient()
const query = useCollectionListQuery({ slug: context.slug })

const invalidateList = async (page?: number) => {
const additionalKeys = page ? [{ query: { page } }] : []
const invalidateList = async () => {
await queryClient.invalidateQueries({
queryKey: ['GET', `/${context.slug}`, ...additionalKeys],
queryKey: query.queryKey,
exact: false,
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ export function useCollectionListQuery(
search: args.search ?? search,
sort: sort,
}
const fullQueryKey = ['GET', `/${args.slug}`, { query: queryKey }] as const

const query: UseQueryResult<CollectionListResponse> = useQuery({
queryKey: ['GET', `/${args.slug}`, { query: queryKey }] as const,
queryKey: fullQueryKey,
queryFn: async (context) => {
const [, , payload] = context.queryKey
const params = new URLSearchParams([
Expand Down Expand Up @@ -50,5 +51,5 @@ export function useCollectionListQuery(
placeholderData: keepPreviousData,
})

return query
return { ...query, queryKey: fullQueryKey }
}
Loading