Skip to content
Open
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
36 changes: 14 additions & 22 deletions src/classes/Builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { toArray } from "@/actions/presenter";
import { clearSortsAction, removeSortAction, sortAction } from "@/actions/sort";
import { whenAction } from "@/actions/when";
import {
type AliasAttribute,
type BaseConfig,
type Field,
type FilterValue,
Expand Down Expand Up @@ -164,9 +165,7 @@ export class Builder<
}

filter<TFilter extends FilterValue>(
attribute: Aliases extends object
? (keyof Aliases & string) | string
: string,
attribute: AliasAttribute<Aliases>,
value: TFilter,
...args: TFilter extends OperatorType
? [override: FilterValue]
Expand Down Expand Up @@ -232,9 +231,7 @@ export class Builder<
return hasField(fields, this.state);
}

hasFilter(
...filters: (Aliases extends object ? keyof Aliases | string : string)[]
): boolean {
hasFilter(...filters: AliasAttribute<Aliases>[]): boolean {
return hasFilter(filters, this.state);
}

Expand All @@ -246,11 +243,7 @@ export class Builder<
return hasParam(params, this.state);
}

hasSort(
...sorts: (Aliases extends object
? (keyof Aliases & string) | string
: string)[]
): boolean {
hasSort(...sorts: AliasAttribute<Aliases>[]): boolean {
return hasSort(sorts, this.state);
}

Expand All @@ -277,7 +270,9 @@ export class Builder<
if (this.state.pagination?.page && this.state.pagination.page >= 1) {
this.setState((s) =>
pageAction(
/* v8 ignore next */ s.pagination?.page !== undefined ? s.pagination.page + 1 : 1,
/* v8 ignore next */ s.pagination?.page !== undefined
? s.pagination.page + 1
: 1,
s,
),
);
Expand All @@ -295,7 +290,10 @@ export class Builder<
previousPage(): QueryBuilder<Aliases> {
if (this.state.pagination?.page && this.state.pagination.page > 1) {
this.setState((s) =>
pageAction(/* v8 ignore next */ s.pagination?.page ? s.pagination.page - 1 : 1, s),
pageAction(
/* v8 ignore next */ s.pagination?.page ? s.pagination.page - 1 : 1,
s,
),
);
}
return this;
Expand All @@ -310,9 +308,7 @@ export class Builder<
}

removeFilter(
...filtersToRemove: (Aliases extends object
? (keyof Aliases & string) | string
: string)[]
...filtersToRemove: AliasAttribute<Aliases>[]
): QueryBuilder<Aliases> {
if (
this.state.filters.some((filter) =>
Expand Down Expand Up @@ -351,9 +347,7 @@ export class Builder<
}

removeSort(
...sortsToRemove: (Aliases extends object
? (keyof Aliases & string) | string
: string)[]
...sortsToRemove: AliasAttribute<Aliases>[]
): QueryBuilder<Aliases> {
const hasSortToRemove = sortsToRemove.some((s) =>
this.state.sorts.some((sort) => sort.attribute === s),
Expand All @@ -379,9 +373,7 @@ export class Builder<
}

sort(
attribute: Aliases extends object
? (keyof Aliases & string) | string
: string,
attribute: AliasAttribute<Aliases>,
direction?: "asc" | "desc",
): QueryBuilder<Aliases> {
const currentSort = this.state.sorts.find((f) => f.attribute === attribute);
Expand Down
15 changes: 9 additions & 6 deletions src/hooks/useQueryBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { Builder } from "@/classes/Builder";
import { useMount } from "@/hooks/useMount";
import { type BaseConfig, type QueryBuilder } from "src/types";
import { type BaseConfig, type QueryBuilder } from "@/types";

export const useQueryBuilder = <
Aliases extends Record<string, string> | undefined = undefined,
Expand All @@ -12,11 +11,15 @@ export const useQueryBuilder = <

const [, setReRendersCounter] = useState(0);

useMount(() => {
builder.current.addSubscriber(() => {
useEffect(() => {
const instance = builder.current;
const id = instance.addSubscriber(() => {
setReRendersCounter((r) => r + 1);
});
});
return () => {
instance.removeSubscriber(id);
};
}, []);

return builder.current;
};
36 changes: 12 additions & 24 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
export type AliasAttribute<Al> = Al extends object
? (keyof Al & string) | string
: string;

export interface Sort<T> {
attribute: T extends object ? (keyof T & string) | string : string;
attribute: AliasAttribute<T>;
direction: "asc" | "desc";
}

Expand All @@ -25,7 +29,7 @@ export const FilterOperator = {
export type OperatorType = (typeof FilterOperator)[keyof typeof FilterOperator];

export interface Filter<Al> {
attribute: Al extends object ? (keyof Al & string) | string : string;
attribute: AliasAttribute<Al>;
value: (string | number)[];
/** Filter comparison operator. Defaults to `FilterOperator.Equals` (`=`) when omitted. */
operator?: OperatorType;
Expand Down Expand Up @@ -84,33 +88,25 @@ export interface QueryBuilder<
* builder.filter("status", "inactive", true)
*/
filter: <TFilter extends FilterValue>(
attribute: AliasType extends object
? (keyof AliasType & string) | string
: string,
attribute: AliasAttribute<AliasType>,
value: TFilter,
...override: TFilter extends OperatorType
? [override: FilterValue]
: [override?: boolean]
) => QueryBuilder<AliasType>;
removeFilter: (
...filtersToRemove: (AliasType extends object
? (keyof AliasType & string) | string
: string)[]
...filtersToRemove: AliasAttribute<AliasType>[]
) => QueryBuilder<AliasType>;
clearFilters: () => QueryBuilder<AliasType>;
include: (...includes: Include[]) => QueryBuilder<AliasType>;
removeInclude: (...includesToRemove: Include[]) => QueryBuilder<AliasType>;
clearIncludes: () => QueryBuilder<AliasType>;
sort: (
attribute: AliasType extends object
? (keyof AliasType & string) | string
: string,
attribute: AliasAttribute<AliasType>,
direction?: "asc" | "desc",
) => QueryBuilder<AliasType>;
removeSort: (
...attributeToRemove: (AliasType extends object
? (keyof AliasType & string) | string
: string)[]
...attributeToRemove: AliasAttribute<AliasType>[]
) => QueryBuilder<AliasType>;
clearSorts: () => QueryBuilder<AliasType>;
build: () => string;
Expand All @@ -131,16 +127,8 @@ export interface QueryBuilder<
callback: (state: GlobalState<AliasType>) => void,
) => QueryBuilder<AliasType>;
toArray: () => string[];
hasFilter: (
...attribute: (AliasType extends object
? keyof AliasType | string
: string)[]
) => boolean;
hasSort: (
...attribute: (AliasType extends object
? (keyof AliasType & string) | string
: string)[]
) => boolean;
hasFilter: (...attribute: AliasAttribute<AliasType>[]) => boolean;
hasSort: (...attribute: AliasAttribute<AliasType>[]) => boolean;
hasInclude: (...includes: Include[]) => boolean;
hasField: (...fields: Field[]) => boolean;
hasParam: (...key: string[]) => boolean;
Expand Down
11 changes: 11 additions & 0 deletions tests/Units/hooks/useQueryBuilder.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,15 @@ describe("useQueryBuilder", () => {

expect(result.current.build()).toBe("?filter[full_name]=John");
});

it("should not throw when mutating builder after unmount", () => {
const { result, unmount } = renderHook(() => useQueryBuilder());
const builder = result.current;

unmount();

expect(() => {
builder.filter("name", "John");
}).not.toThrow();
});
});
Loading