Skip to content

Commit

Permalink
feat: allow tuples of any size in dynamic keys
Browse files Browse the repository at this point in the history
* feat: accept tuple of any size in dynamic keys

* docs: update README with new examples

* chore: bump package version
  • Loading branch information
lukemorales authored Aug 29, 2022
1 parent 0ab52ed commit d4b95e2
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 31 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]);

Expand All @@ -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 <div> {/* render product page */} </div>;
Expand Down Expand Up @@ -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']
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
49 changes: 42 additions & 7 deletions src/create-query-keys.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 extends string>(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 },
]);
});
});

Expand Down
21 changes: 10 additions & 11 deletions src/create-query-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
FactoryObject,
FactoryOutput,
FactoryOutputCallback,
KeyScopeTuple,
KeyScopeValue,
QueryKeyFactoryResult,
ValidateFactory,
Expand Down Expand Up @@ -31,20 +32,18 @@ export function createQueryKeys<Key extends string, FactorySchema extends Factor
scope: Scope,
scopeValue: ScopeValue,
): readonly [Key, Scope, ScopeValue];
function createKey<Scope extends string, ScopeValue extends string | number, DeeperScopeValue extends KeyScopeValue>(
function createKey<Scope extends string, ScopeValue extends KeyScopeTuple>(
scope: Scope,
scopeValue: ScopeValue,
deeperScopeValue: DeeperScopeValue,
): readonly [Key, Scope, ScopeValue, DeeperScopeValue];
): readonly [Key, Scope, ...ScopeValue];

function createKey<Scope extends string, ScopeValue extends KeyScopeValue, DeeperScopeValue extends KeyScopeValue>(
function createKey<Scope extends string, ScopeValue extends KeyScopeValue | KeyScopeTuple>(
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;
Expand All @@ -62,14 +61,14 @@ export function createQueryKeys<Key extends string, FactorySchema extends Factor
let yieldValue: any;

if (typeof currentValue === 'function') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ResultCallback = FactoryOutputCallback<Key, typeof key, any>;
type ResultCallback = FactoryOutputCallback<Key, typeof key, typeof currentValue>;

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);
Expand Down
21 changes: 14 additions & 7 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
type AnyObject = Record<string, unknown>;

type FactoryCallback = (...args: any[]) => KeyScopeValue | [string | number, KeyScopeValue];
export type KeyScopeTuple = [KeyScopeValue | undefined, ...Array<KeyScopeValue | undefined>];

type FactoryCallback = (...args: any[]) => KeyScopeValue | KeyScopeTuple;

export type KeyScopeValue = string | number | boolean | AnyObject;

Expand All @@ -17,18 +19,23 @@ export type ValidateFactory<Schema extends FactoryObject> = Schema extends {
? ValidateSchema<Schema>
: Schema;

export type FactoryOutputCallback<Key, Property, Callback extends FactoryCallback> = {
(...args: Parameters<Callback>): ReturnType<Callback> extends [infer ScopeValue, infer DeeperScopeValue]
? readonly [Key, Property, ScopeValue, DeeperScopeValue]
: ReturnType<Callback> extends AnyObject
export type FactoryOutputCallback<
Key,
Property,
Callback extends FactoryCallback,
CallbackResult extends ReturnType<Callback> = ReturnType<Callback>,
> = {
(...args: Parameters<Callback>): CallbackResult extends [...infer TupleResult]
? readonly [Key, Property, ...TupleResult]
: CallbackResult extends AnyObject
? readonly [
Key,
Property,
{
[SubKey in keyof ReturnType<Callback>]: ReturnType<Callback>[SubKey];
[SubKey in keyof CallbackResult]: CallbackResult[SubKey];
},
]
: readonly [Key, Property, ReturnType<Callback>];
: readonly [Key, Property, CallbackResult];
toScope: () => readonly [Key, Property];
};

Expand Down

0 comments on commit d4b95e2

Please sign in to comment.