Skip to content

Commit c4bd16c

Browse files
max-scoppMax Scopp
andauthored
feat(client): Allow the client to be used more flexibly (#274)
* feat(client): Allow the client to be used more flexibly * chore: extend test cases * docs(client): extend documentation for GridifyQueryBuilder options and methods --------- Co-authored-by: Max Scopp <mscopp@tesla.com>
1 parent 0eba4c2 commit c4bd16c

File tree

5 files changed

+167
-25
lines changed

5 files changed

+167
-25
lines changed

client/src/GridifyQueryBuilder.ts

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11
import { ConditionalOperator, LogicalOperator } from "./GridifyOperator";
2+
import { GridifyQueryBuilderOptions } from "./GridifyQueryBuilderOptions";
23
import { IGridifyQuery } from "./IGridifyQuery";
34

45
export class GridifyQueryBuilder {
5-
private query: IGridifyQuery = {
6+
protected query: IGridifyQuery = {
67
page: 1,
78
pageSize: 20,
89
orderBy: "",
910
filter: "",
1011
};
1112

12-
private filteringExpressions: IExpression[] = [];
13+
protected filteringExpressions: IExpression[] = [];
14+
15+
constructor(protected readonly options: GridifyQueryBuilderOptions = {}) {
16+
if (options.from) {
17+
const builder = options.from;
18+
19+
this.query = { ...builder.query };
20+
this.filteringExpressions = builder.filteringExpressions.map((exp) => ({ ...exp }));
21+
}
22+
}
1323

1424
setPage(page: number): GridifyQueryBuilder {
1525
this.query.page = page;
@@ -69,16 +79,21 @@ export class GridifyQueryBuilder {
6979
return this;
7080
}
7181

72-
and(): GridifyQueryBuilder {
82+
and(optional?: boolean): GridifyQueryBuilder {
7383
this.filteringExpressions.push({
7484
value: LogicalOperator.And,
7585
type: "op",
86+
optional
7687
});
7788
return this;
7889
}
7990

80-
or(): GridifyQueryBuilder {
81-
this.filteringExpressions.push({ value: LogicalOperator.Or, type: "op" });
91+
or(optional?: boolean): GridifyQueryBuilder {
92+
this.filteringExpressions.push({
93+
value: LogicalOperator.Or,
94+
type: "op",
95+
optional
96+
});
8297
return this;
8398
}
8499

@@ -96,6 +111,9 @@ export class GridifyQueryBuilder {
96111

97112
//,
98113
if (previousType === null && exp.type === "op") {
114+
if (exp.optional) {
115+
return
116+
}
99117
throw new Error("expression cannot start with a logical operator");
100118
}
101119

@@ -108,13 +126,19 @@ export class GridifyQueryBuilder {
108126

109127
// ,,
110128
if (previousType === "op" && exp.type === "op") {
129+
if (exp.optional) {
130+
return
131+
}
111132
throw new Error(
112133
"consecutive operators are not allowed, consider adding a filter"
113134
);
114135
}
115136

116137
// (,
117138
if (previousType === "startGroup" && exp.type === "op") {
139+
if (exp.optional) {
140+
return
141+
}
118142
throw new Error(
119143
"logical operator immediately after startGroup is not allowed"
120144
);
@@ -126,8 +150,10 @@ export class GridifyQueryBuilder {
126150
}
127151

128152
// ()
129-
if (previousType === "startGroup" && exp.type === "endGroup") {
130-
throw new Error("Empty groups are not allowed");
153+
if (!this.options.allowEmptyGroups) {
154+
if (previousType === "startGroup" && exp.type === "endGroup") {
155+
throw new Error("Empty groups are not allowed");
156+
}
131157
}
132158

133159
// )(
@@ -143,11 +169,15 @@ export class GridifyQueryBuilder {
143169
throw new Error("Group not properly closed");
144170
}
145171

172+
// postprocess
173+
this.query.filter = this.query.filter?.replace(/[,|]?\(\)/gi, "");
174+
146175
return this.query;
147176
}
148177
}
149178

150179
interface IExpression {
151180
value: string;
152181
type: "filter" | "op" | "startGroup" | "endGroup";
182+
optional?: boolean
153183
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { GridifyQueryBuilder } from "./GridifyQueryBuilder";
2+
3+
export interface GridifyQueryBuilderOptions {
4+
from?: GridifyQueryBuilder;
5+
allowEmptyGroups?: boolean;
6+
}

client/test/GridifyQueryBuilder.test.ts

Lines changed: 95 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { GridifyQueryBuilder } from "../src/GridifyQueryBuilder";
21
import { ConditionalOperator as op } from "../src/GridifyOperator";
2+
import { GridifyQueryBuilder } from "../src/GridifyQueryBuilder";
33

44
describe("GridifyQueryBuilder", () => {
55
it("should build a simple query", () => {
@@ -18,6 +18,93 @@ describe("GridifyQueryBuilder", () => {
1818
});
1919
});
2020

21+
it("can be extended by the user", () => {
22+
const query = new GridifyQueryBuilder()
23+
.setPage(1)
24+
.setPageSize(20)
25+
.addOrderBy("name")
26+
.addCondition("age", op.GreaterThan, 30);
27+
28+
class CustomQueryImpl extends GridifyQueryBuilder {
29+
30+
override build() {
31+
return {
32+
...super.build(),
33+
pageSize: this.query.pageSize + 1,
34+
addition: 1
35+
}
36+
}
37+
}
38+
39+
const customQuery = new CustomQueryImpl({
40+
from: query
41+
}).build();
42+
43+
expect(customQuery).toEqual({
44+
page: 1,
45+
pageSize: 21,
46+
orderBy: "name",
47+
filter: "age>30",
48+
addition: 1
49+
});
50+
});
51+
52+
it("should be able to inherit existing instances", () => {
53+
const query = new GridifyQueryBuilder()
54+
.setPage(1)
55+
.setPageSize(20)
56+
.addOrderBy("name")
57+
.addCondition("age", op.GreaterThan, 30);
58+
59+
const cloned = new GridifyQueryBuilder({
60+
from: query
61+
});
62+
63+
expect(cloned.build()).toEqual(query.build());
64+
});
65+
66+
it("should remove all empty groups", () => {
67+
const query = new GridifyQueryBuilder({
68+
allowEmptyGroups: true
69+
})
70+
.setPage(1)
71+
.setPageSize(20)
72+
.addOrderBy("name")
73+
.startGroup().endGroup().or(true)
74+
.startGroup().endGroup()
75+
.build();
76+
77+
78+
expect(query).toEqual({
79+
page: 1,
80+
pageSize: 20,
81+
orderBy: "name",
82+
filter: "",
83+
});
84+
});
85+
86+
it("should be able to skip empty groups", () => {
87+
const query = new GridifyQueryBuilder({
88+
allowEmptyGroups: true
89+
})
90+
.setPage(1)
91+
.setPageSize(20)
92+
.addOrderBy("name")
93+
.startGroup()
94+
.addCondition("age", op.GreaterThan, 30)
95+
.endGroup()
96+
.and(true)
97+
.startGroup().endGroup().build();
98+
99+
100+
expect(query).toEqual({
101+
page: 1,
102+
pageSize: 20,
103+
orderBy: "name",
104+
filter: "(age>30)",
105+
});
106+
});
107+
21108
it("should build a complex query with logical operators and groups", () => {
22109
const query = new GridifyQueryBuilder()
23110
.setPage(2)
@@ -85,13 +172,13 @@ describe("GridifyQueryBuilder Validation", () => {
85172
it("should allow nested balanced parentheses", () => {
86173
const query = new GridifyQueryBuilder()
87174
.startGroup()
88-
.addCondition("field", op.Equal, "value")
89-
.and()
90-
.startGroup()
91-
.addCondition("field", op.Equal, "value")
92-
.or()
93-
.addCondition("field", op.Equal, "value")
94-
.endGroup()
175+
.addCondition("field", op.Equal, "value")
176+
.and()
177+
.startGroup()
178+
.addCondition("field", op.Equal, "value")
179+
.or()
180+
.addCondition("field", op.Equal, "value")
181+
.endGroup()
95182
.endGroup()
96183
.build();
97184

client/tsconfig.json

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
{
22
"compilerOptions": {
3-
"target": "es5",
4-
"module": "ES2015",
5-
"declaration": true,
6-
"outDir": "./dist",
7-
"esModuleInterop": true,
8-
"strict": true,
9-
"sourceMap": true
3+
"target": "ES5",
4+
"lib": ["ES2017"],
5+
"module": "ES2015",
6+
"declaration": true,
7+
"outDir": "./dist",
8+
"esModuleInterop": true,
9+
"strict": true,
10+
"sourceMap": true
1011
},
1112
"include": ["src/**/*"],
1213
"exclude": ["node_modules", "test", "**/*.test.ts"]
13-
}
14+
}

docs/pages/guide/extensions/gridify-client.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,21 @@ npm i gridify-client
1717
The `GridifyQueryBuilder` interface represents the methods available for constructing dynamic queries using the Gridify
1818
Client library.
1919

20+
When creating a new instance of `GridifyQueryBuilder`, you can pass an optional `GridifyQueryBuilderOptions` object to the constructor. This allows you to configure advanced behaviors:
21+
22+
- `from`: Clone an existing builder's state.
23+
- `allowEmptyGroups`: Allow empty logical groups in the filter expression (default is `false`).
24+
25+
**Example:**
26+
27+
```ts
28+
import { GridifyQueryBuilder } from "gridify-client";
29+
30+
const builder = new GridifyQueryBuilder({
31+
allowEmptyGroups: true
32+
});
33+
```
34+
2035
The following table describes the methods available in the GridifyQueryBuilder interface for constructing dynamic queries.
2136

2237
| Method | Parameter | Description |
@@ -27,10 +42,12 @@ The following table describes the methods available in the GridifyQueryBuilder i
2742
| addCondition | field, operator, value, caseSensitive, escapeValue | Add filtering conditions. `caseSensitive` and `escapeValue` are optional parameters. |
2843
| startGroup | - | Start a logical grouping of conditions. |
2944
| endGroup | - | End the current logical group. |
30-
| and | - | Add the logical AND operator. |
31-
| or | - | Add the logical OR operator. |
45+
| and | optional?: boolean | Add the logical AND operator. If `optional` is `true`, the operator will be ignored if it would be invalid in the current context. |
46+
| or | optional?: boolean | Add the logical OR operator. If `optional` is `true`, the operator will be ignored if it would be invalid in the current context. |
3247
| build | - | Build and retrieve the constructed query. |
3348

49+
> **Note:**
50+
> The `and` and `or` methods accepts an optional boolean parameter. If set to `true`, the logical operator will be treated as optional and ignored if it would result in an invalid expression (such as being the first operator or consecutive with another operator).
3451
3552
## Conditional Operators
3653

@@ -86,3 +103,4 @@ Output:
86103
"filter": "(age<50|name^A),isActive=true"
87104
}
88105
```
106+

0 commit comments

Comments
 (0)