From d4b95e26e1e7172f4344135e02b59dd3f39dc263 Mon Sep 17 00:00:00 2001 From: Luke Morales Date: Mon, 29 Aug 2022 20:47:36 -0300 Subject: [PATCH] feat: allow tuples of any size in dynamic keys * feat: accept tuple of any size in dynamic keys * docs: update README with new examples * chore: bump package version --- CHANGELOG.md | 6 +++++ README.md | 18 +++++++++---- package.json | 2 +- src/create-query-keys.spec.ts | 49 ++++++++++++++++++++++++++++++----- src/create-query-keys.ts | 21 +++++++-------- src/types.ts | 21 ++++++++++----- 6 files changed, 86 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec51aee..4290d07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # @lukemorales/query-key-factory +## 0.3.0 + +### Minor Changes + +- Allow tuples of any size in dynamic keys + ## 0.2.1 ### Patch Changes diff --git a/README.md b/README.md index 3c9ce02..9bed94c 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,8 @@ const ProductList: FC = () => { useEffect(() => { if (search === '') { - queryClient.invalidateQueries(productKeys.search.toScope()); // invalidate cache only for the search scope + queryClient.invalidateQueries(productKeys.search.toScope()); + // invalidate cache only for the search scope } }, [search]); @@ -73,7 +74,8 @@ const Product: FC = () => { const product = useQuery(productsKeys.byId(productId), fetchProduct); const onAddToCart = () => { - queryClient.invalidateQueries(productsKeys.default); // invalidade cache for entire feature + queryClient.invalidateQueries(productsKeys.default); + // invalidade cache for entire feature } return
{/* render product page */}
; @@ -110,12 +112,18 @@ const todosKeys = createQueryKeys('todos', { single: (id: string) => id, tag: (tagId: string) => ({ tagId }), search: (query: string, limit: number) => [query, { limit }], + filter: ({ filter, status, limit }: FilterOptions) => [filter, status, limit], }); -todosKeys.single('todo_id'); // ['todos', 'single', 'todo_id'] -todosKeys.tag('tag_homework'); // ['todos', 'tag', { tagId: 'tag_homework' }] -todosKeys.search('learn tanstack query', 15); // ['todos', 'search', 'learn tanstack query', { limit: 15 }] +todosKeys.single('todo_id'); +// ['todos', 'single', 'todo_id'] +todosKeys.tag('tag_homework'); +// ['todos', 'tag', { tagId: 'tag_homework' }] +todosKeys.search('learn tanstack query', 15); +// ['todos', 'search', 'learn tanstack query', { limit: 15 }] +todosKeys.filter('not-owned-by-me', 'done', 15); +// ['todos', 'filter', 'not-owned-by-me', 'done', 15] todosKeys.single.toScope(); // ['todos', 'single'] todosKeys.tag.toScope(); // ['todos', 'tag'] diff --git a/package.json b/package.json index 77e9b18..413a45c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lukemorales/query-key-factory", - "version": "0.2.1", + "version": "0.3.0", "repository": { "type": "git", "url": "https://github.com/lukemorales/query-key-factory.git" diff --git a/src/create-query-keys.spec.ts b/src/create-query-keys.spec.ts index bd896a3..a2edecf 100644 --- a/src/create-query-keys.spec.ts +++ b/src/create-query-keys.spec.ts @@ -110,18 +110,53 @@ describe('createQueryKeys', () => { }); describe('when the function returns a tuple', () => { + type TodoStatus = 'done' | 'ongoing'; + + type Test = { + id: string; + preview: boolean; + status: TodoStatus; + tasksPerPage: number; + }; + it('creates a function that returns a formatted query key when the result is an array', () => { const queryKeys = createQueryKeys('master-key', { - todoWithPreview: (id: Id, preview: boolean) => [id, { preview }], + todoKeyWithTuple: ({ id, preview, status, tasksPerPage }: Test) => [id, preview, status, tasksPerPage], + todoKeyWithRecord: (id: string, preview: boolean) => [id, { preview }], }); - expect(typeof queryKeys.todoWithPreview).toBe('function'); - - const generatedKey = queryKeys.todoWithPreview('todo-id', false); + expect(typeof queryKeys.todoKeyWithTuple).toBe('function'); + expect(typeof queryKeys.todoKeyWithRecord).toBe('function'); - expect(Array.isArray(generatedKey)).toBeTruthy(); - expect(generatedKey).toHaveLength(4); - expect(generatedKey).toStrictEqual(['master-key', 'todoWithPreview', 'todo-id', { preview: false }]); + const generatedKeyWithTuple = queryKeys.todoKeyWithTuple({ + // ^? + id: 'ongoing-todo-id', + preview: true, + status: 'ongoing', + tasksPerPage: 3, + }); + const generatedKeyWithRecord = queryKeys.todoKeyWithRecord('todo-id', false); + // ^? + + expect(Array.isArray(generatedKeyWithTuple)).toBeTruthy(); + expect(generatedKeyWithTuple).toHaveLength(6); + expect(generatedKeyWithTuple).toStrictEqual([ + 'master-key', + 'todoKeyWithTuple', + 'ongoing-todo-id', + true, + 'ongoing', + 3, + ]); + + expect(Array.isArray(generatedKeyWithRecord)).toBeTruthy(); + expect(generatedKeyWithRecord).toHaveLength(4); + expect(generatedKeyWithRecord).toStrictEqual([ + 'master-key', + 'todoKeyWithRecord', + 'todo-id', + { preview: false }, + ]); }); }); diff --git a/src/create-query-keys.ts b/src/create-query-keys.ts index 556f3b3..5c8b7db 100644 --- a/src/create-query-keys.ts +++ b/src/create-query-keys.ts @@ -3,6 +3,7 @@ import type { FactoryObject, FactoryOutput, FactoryOutputCallback, + KeyScopeTuple, KeyScopeValue, QueryKeyFactoryResult, ValidateFactory, @@ -31,20 +32,18 @@ export function createQueryKeys( + function createKey( scope: Scope, scopeValue: ScopeValue, - deeperScopeValue: DeeperScopeValue, - ): readonly [Key, Scope, ScopeValue, DeeperScopeValue]; + ): readonly [Key, Scope, ...ScopeValue]; - function createKey( + function createKey( scope: Scope, scopeValue?: ScopeValue, - deeperScopeValue?: DeeperScopeValue, - ): readonly [Key, Scope] | readonly [Key, Scope, ScopeValue] | readonly [Key, Scope, ScopeValue, DeeperScopeValue] { + ): readonly [Key, Scope] | readonly [Key, Scope, ScopeValue] | readonly [Key, Scope, ...KeyScopeTuple[]] { if (scopeValue != null) { - if (deeperScopeValue != null) { - return [defaultKey, scope, scopeValue, deeperScopeValue] as const; + if (Array.isArray(scopeValue)) { + return [defaultKey, scope, ...scopeValue] as const; } return [defaultKey, scope, scopeValue] as const; @@ -62,14 +61,14 @@ export function createQueryKeys; + type ResultCallback = FactoryOutputCallback; const resultCallback: ResultCallback = (...args) => { const result = currentValue(...args); + // necessary for correct createKey overload to be called if (Array.isArray(result)) { - return createKey(key, result[0], result[1]); + return createKey(key, result); } return createKey(key, result); diff --git a/src/types.ts b/src/types.ts index 8fb5d21..7198c16 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,7 +1,9 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ type AnyObject = Record; -type FactoryCallback = (...args: any[]) => KeyScopeValue | [string | number, KeyScopeValue]; +export type KeyScopeTuple = [KeyScopeValue | undefined, ...Array]; + +type FactoryCallback = (...args: any[]) => KeyScopeValue | KeyScopeTuple; export type KeyScopeValue = string | number | boolean | AnyObject; @@ -17,18 +19,23 @@ export type ValidateFactory = Schema extends { ? ValidateSchema : Schema; -export type FactoryOutputCallback = { - (...args: Parameters): ReturnType extends [infer ScopeValue, infer DeeperScopeValue] - ? readonly [Key, Property, ScopeValue, DeeperScopeValue] - : ReturnType extends AnyObject +export type FactoryOutputCallback< + Key, + Property, + Callback extends FactoryCallback, + CallbackResult extends ReturnType = ReturnType, +> = { + (...args: Parameters): CallbackResult extends [...infer TupleResult] + ? readonly [Key, Property, ...TupleResult] + : CallbackResult extends AnyObject ? readonly [ Key, Property, { - [SubKey in keyof ReturnType]: ReturnType[SubKey]; + [SubKey in keyof CallbackResult]: CallbackResult[SubKey]; }, ] - : readonly [Key, Property, ReturnType]; + : readonly [Key, Property, CallbackResult]; toScope: () => readonly [Key, Property]; };