@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.
@@ -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
- if (rawFilter.includes(operator)) {
932
- const [field, valuesRaw] = rawFilter.split(operator, 2);
933
- const values = valuesRaw ? valuesRaw.split('|').map(decodeURIComponent) : [];
934
- res.locals.filters.push({ field: field, operator, values });
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
- if (rawFilter.includes(operator)) {
15
- const [field, valuesRaw] = rawFilter.split(operator, 2);
16
- const values = valuesRaw ? valuesRaw.split('|').map(decodeURIComponent) : [];
17
- res.locals.filters.push({ field: field, operator, values });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unito/integration-sdk",
3
- "version": "5.1.1",
3
+ "version": "5.2.1",
4
4
  "description": "Integration SDK",
5
5
  "type": "module",
6
6
  "types": "dist/src/index.d.ts",
@@ -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
- if (rawFilter.includes(operator)) {
46
- const [field, valuesRaw] = rawFilter.split(operator, 2);
47
- const values = valuesRaw ? valuesRaw.split('|').map(decodeURIComponent) : [];
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
- res.locals.filters.push({ field: field!, operator, values });
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
- break;
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;