@unito/integration-sdk 5.1.1 → 5.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/index.cjs
CHANGED
|
@@ -927,14 +927,24 @@ function extractFilters(req, res, next) {
|
|
|
927
927
|
res.locals.filters = [];
|
|
928
928
|
if (typeof rawFilters === 'string') {
|
|
929
929
|
for (const rawFilter of rawFilters.split(',')) {
|
|
930
|
+
// Find the operator that appears earliest in the string.
|
|
931
|
+
// When two operators start at the same index (e.g. ">=" and ">" both at position 5),
|
|
932
|
+
// the longer one wins because ORDERED_OPERATORS is sorted longest-first.
|
|
933
|
+
let bestIdx = -1;
|
|
934
|
+
let bestOperator;
|
|
930
935
|
for (const operator of ORDERED_OPERATORS) {
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
break;
|
|
936
|
+
const idx = rawFilter.indexOf(operator);
|
|
937
|
+
if (idx !== -1 && (bestIdx === -1 || idx < bestIdx)) {
|
|
938
|
+
bestIdx = idx;
|
|
939
|
+
bestOperator = operator;
|
|
936
940
|
}
|
|
937
941
|
}
|
|
942
|
+
if (bestIdx !== -1 && bestOperator) {
|
|
943
|
+
const field = rawFilter.slice(0, bestIdx);
|
|
944
|
+
const valuesRaw = rawFilter.slice(bestIdx + bestOperator.length);
|
|
945
|
+
const values = valuesRaw ? valuesRaw.split('|').map(decodeURIComponent) : [];
|
|
946
|
+
res.locals.filters.push({ field, operator: bestOperator, values });
|
|
947
|
+
}
|
|
938
948
|
}
|
|
939
949
|
}
|
|
940
950
|
next();
|
|
@@ -10,14 +10,24 @@ function extractFilters(req, res, next) {
|
|
|
10
10
|
res.locals.filters = [];
|
|
11
11
|
if (typeof rawFilters === 'string') {
|
|
12
12
|
for (const rawFilter of rawFilters.split(',')) {
|
|
13
|
+
// Find the operator that appears earliest in the string.
|
|
14
|
+
// When two operators start at the same index (e.g. ">=" and ">" both at position 5),
|
|
15
|
+
// the longer one wins because ORDERED_OPERATORS is sorted longest-first.
|
|
16
|
+
let bestIdx = -1;
|
|
17
|
+
let bestOperator;
|
|
13
18
|
for (const operator of ORDERED_OPERATORS) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
break;
|
|
19
|
+
const idx = rawFilter.indexOf(operator);
|
|
20
|
+
if (idx !== -1 && (bestIdx === -1 || idx < bestIdx)) {
|
|
21
|
+
bestIdx = idx;
|
|
22
|
+
bestOperator = operator;
|
|
19
23
|
}
|
|
20
24
|
}
|
|
25
|
+
if (bestIdx !== -1 && bestOperator) {
|
|
26
|
+
const field = rawFilter.slice(0, bestIdx);
|
|
27
|
+
const valuesRaw = rawFilter.slice(bestIdx + bestOperator.length);
|
|
28
|
+
const values = valuesRaw ? valuesRaw.split('|').map(decodeURIComponent) : [];
|
|
29
|
+
res.locals.filters.push({ field, operator: bestOperator, values });
|
|
30
|
+
}
|
|
21
31
|
}
|
|
22
32
|
}
|
|
23
33
|
next();
|
|
@@ -31,6 +31,56 @@ describe('filters middleware', () => {
|
|
|
31
31
|
filters: [{ field: 'status', operator: OperatorTypes.EQUAL, values: ['foo,bar!!,?baz=!>qux'] }],
|
|
32
32
|
});
|
|
33
33
|
});
|
|
34
|
+
it('parses multi-value filters separated by pipe', () => {
|
|
35
|
+
const request = {
|
|
36
|
+
query: { filter: 'status=/statuses/Done|/statuses/InProgress' },
|
|
37
|
+
};
|
|
38
|
+
const response = { locals: {} };
|
|
39
|
+
extractFilters(request, response, () => { });
|
|
40
|
+
assert.deepEqual(response.locals, {
|
|
41
|
+
filters: [{ field: 'status', operator: OperatorTypes.EQUAL, values: ['/statuses/Done', '/statuses/InProgress'] }],
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
it('preserves base64 padding (=) in filter values and does not lose subsequent pipe-separated values', () => {
|
|
45
|
+
// Base64 values end with == which contains the = operator character.
|
|
46
|
+
// split('=', 2) incorrectly truncates after the second = in the string,
|
|
47
|
+
// discarding the | separator and subsequent values.
|
|
48
|
+
const request = {
|
|
49
|
+
query: {
|
|
50
|
+
filter: 'category=/choices/QnVzaW5lc3MgU3lzdGVtOi06QnVzaW5lc3MgU3lzdGVt|/choices/QXBwbGljYXRpb246LTpBcHBsaWNhdGlvbg==,' +
|
|
51
|
+
'subcategory=/choices/U0FQOi06QnVzaW5lc3MgU3lzdGVtX1NBUA==|/choices/UmVkd29vZCBSTUo6LTpyZWR3b29kIHJtag==',
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
const response = { locals: {} };
|
|
55
|
+
extractFilters(request, response, () => { });
|
|
56
|
+
assert.deepEqual(response.locals, {
|
|
57
|
+
filters: [
|
|
58
|
+
{
|
|
59
|
+
field: 'category',
|
|
60
|
+
operator: OperatorTypes.EQUAL,
|
|
61
|
+
values: [
|
|
62
|
+
'/choices/QnVzaW5lc3MgU3lzdGVtOi06QnVzaW5lc3MgU3lzdGVt',
|
|
63
|
+
'/choices/QXBwbGljYXRpb246LTpBcHBsaWNhdGlvbg==',
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
field: 'subcategory',
|
|
68
|
+
operator: OperatorTypes.EQUAL,
|
|
69
|
+
values: ['/choices/U0FQOi06QnVzaW5lc3MgU3lzdGVtX1NBUA==', '/choices/UmVkd29vZCBSTUo6LTpyZWR3b29kIHJtag=='],
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
it('matches the earliest operator even when operator characters appear in the value', () => {
|
|
75
|
+
const request = {
|
|
76
|
+
query: { filter: 'description=contains>=text' },
|
|
77
|
+
};
|
|
78
|
+
const response = { locals: {} };
|
|
79
|
+
extractFilters(request, response, () => { });
|
|
80
|
+
assert.deepEqual(response.locals, {
|
|
81
|
+
filters: [{ field: 'description', operator: OperatorTypes.EQUAL, values: ['contains>=text'] }],
|
|
82
|
+
});
|
|
83
|
+
});
|
|
34
84
|
it('no data', () => {
|
|
35
85
|
const request = { query: {} };
|
|
36
86
|
const response = { locals: {} };
|
package/package.json
CHANGED
|
@@ -41,15 +41,26 @@ function extractFilters(req: Request, res: Response, next: NextFunction) {
|
|
|
41
41
|
|
|
42
42
|
if (typeof rawFilters === 'string') {
|
|
43
43
|
for (const rawFilter of rawFilters.split(',')) {
|
|
44
|
+
// Find the operator that appears earliest in the string.
|
|
45
|
+
// When two operators start at the same index (e.g. ">=" and ">" both at position 5),
|
|
46
|
+
// the longer one wins because ORDERED_OPERATORS is sorted longest-first.
|
|
47
|
+
let bestIdx = -1;
|
|
48
|
+
let bestOperator: OperatorType | undefined;
|
|
49
|
+
|
|
44
50
|
for (const operator of ORDERED_OPERATORS) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
const idx = rawFilter.indexOf(operator);
|
|
52
|
+
if (idx !== -1 && (bestIdx === -1 || idx < bestIdx)) {
|
|
53
|
+
bestIdx = idx;
|
|
54
|
+
bestOperator = operator;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
48
57
|
|
|
49
|
-
|
|
58
|
+
if (bestIdx !== -1 && bestOperator) {
|
|
59
|
+
const field = rawFilter.slice(0, bestIdx);
|
|
60
|
+
const valuesRaw = rawFilter.slice(bestIdx + bestOperator.length);
|
|
61
|
+
const values = valuesRaw ? valuesRaw.split('|').map(decodeURIComponent) : [];
|
|
50
62
|
|
|
51
|
-
|
|
52
|
-
}
|
|
63
|
+
res.locals.filters.push({ field, operator: bestOperator, values });
|
|
53
64
|
}
|
|
54
65
|
}
|
|
55
66
|
}
|
|
@@ -62,6 +62,84 @@ describe('filters middleware', () => {
|
|
|
62
62
|
});
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
+
it('parses multi-value filters separated by pipe', () => {
|
|
66
|
+
const request = {
|
|
67
|
+
query: { filter: 'status=/statuses/Done|/statuses/InProgress' },
|
|
68
|
+
} as express.Request<
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
70
|
+
any,
|
|
71
|
+
object,
|
|
72
|
+
object,
|
|
73
|
+
{ filter: string }
|
|
74
|
+
>;
|
|
75
|
+
const response = { locals: {} } as express.Response;
|
|
76
|
+
|
|
77
|
+
extractFilters(request, response, () => {});
|
|
78
|
+
|
|
79
|
+
assert.deepEqual(response.locals, {
|
|
80
|
+
filters: [{ field: 'status', operator: OperatorTypes.EQUAL, values: ['/statuses/Done', '/statuses/InProgress'] }],
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('preserves base64 padding (=) in filter values and does not lose subsequent pipe-separated values', () => {
|
|
85
|
+
// Base64 values end with == which contains the = operator character.
|
|
86
|
+
// split('=', 2) incorrectly truncates after the second = in the string,
|
|
87
|
+
// discarding the | separator and subsequent values.
|
|
88
|
+
const request = {
|
|
89
|
+
query: {
|
|
90
|
+
filter:
|
|
91
|
+
'category=/choices/QnVzaW5lc3MgU3lzdGVtOi06QnVzaW5lc3MgU3lzdGVt|/choices/QXBwbGljYXRpb246LTpBcHBsaWNhdGlvbg==,' +
|
|
92
|
+
'subcategory=/choices/U0FQOi06QnVzaW5lc3MgU3lzdGVtX1NBUA==|/choices/UmVkd29vZCBSTUo6LTpyZWR3b29kIHJtag==',
|
|
93
|
+
},
|
|
94
|
+
} as express.Request<
|
|
95
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
96
|
+
any,
|
|
97
|
+
object,
|
|
98
|
+
object,
|
|
99
|
+
{ filter: string }
|
|
100
|
+
>;
|
|
101
|
+
const response = { locals: {} } as express.Response;
|
|
102
|
+
|
|
103
|
+
extractFilters(request, response, () => {});
|
|
104
|
+
|
|
105
|
+
assert.deepEqual(response.locals, {
|
|
106
|
+
filters: [
|
|
107
|
+
{
|
|
108
|
+
field: 'category',
|
|
109
|
+
operator: OperatorTypes.EQUAL,
|
|
110
|
+
values: [
|
|
111
|
+
'/choices/QnVzaW5lc3MgU3lzdGVtOi06QnVzaW5lc3MgU3lzdGVt',
|
|
112
|
+
'/choices/QXBwbGljYXRpb246LTpBcHBsaWNhdGlvbg==',
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
field: 'subcategory',
|
|
117
|
+
operator: OperatorTypes.EQUAL,
|
|
118
|
+
values: ['/choices/U0FQOi06QnVzaW5lc3MgU3lzdGVtX1NBUA==', '/choices/UmVkd29vZCBSTUo6LTpyZWR3b29kIHJtag=='],
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('matches the earliest operator even when operator characters appear in the value', () => {
|
|
125
|
+
const request = {
|
|
126
|
+
query: { filter: 'description=contains>=text' },
|
|
127
|
+
} as express.Request<
|
|
128
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
129
|
+
any,
|
|
130
|
+
object,
|
|
131
|
+
object,
|
|
132
|
+
{ filter: string }
|
|
133
|
+
>;
|
|
134
|
+
const response = { locals: {} } as express.Response;
|
|
135
|
+
|
|
136
|
+
extractFilters(request, response, () => {});
|
|
137
|
+
|
|
138
|
+
assert.deepEqual(response.locals, {
|
|
139
|
+
filters: [{ field: 'description', operator: OperatorTypes.EQUAL, values: ['contains>=text'] }],
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
65
143
|
it('no data', () => {
|
|
66
144
|
const request = { query: {} } as express.Request;
|
|
67
145
|
const response = { locals: {} } as express.Response;
|