@schmock/query 1.13.0 → 2.0.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/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/src/index.test.ts +331 -1
- package/src/index.ts +20 -12
- package/src/steps/query-plugin.steps.ts +0 -24
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,UAAU,iBAAiB;IACzB,2CAA2C;IAC3C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wDAAwD;IACxD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,UAAU,cAAc;IACtB,iCAAiC;IACjC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,yBAAyB;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0CAA0C;IAC1C,YAAY,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IAC9B,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6DAA6D;IAC7D,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,UAAU,gBAAgB;IACxB,mCAAmC;IACnC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,6DAA6D;IAC7D,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,UAAU,kBAAkB;IAC1B,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,SAAS,CAAC,EAAE,gBAAgB,CAAC;CAC9B;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,CAoCvE"}
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
function
|
|
1
|
+
var B="2.0.1";function N(c){return{name:"query",version:B,process(h,d){if(!Array.isArray(d))return{context:h,response:d};let j=[...d],k=h.query||{};if(c.filtering)j=E(j,k,c.filtering);if(c.sorting)j=G(j,k,c.sorting);if(c.pagination){let v=H(j,k,c.pagination);return{context:h,response:v}}return{context:h,response:j}}}}function E(c,h,d){let j=d.filterPrefix??"filter",k=c;for(let v of d.allowed){let g=`${j}[${v}]`,f=`${j}.${v}`,b=h[g]??h[f]??h[v];if(b!==void 0)k=k.filter((w)=>{if(typeof w!=="object"||w===null)return!1;let D=w[v];if(D===void 0)return!1;return String(D)===b})}return k}function G(c,h,d){let j=d.sortParam??"sort",k=d.orderParam??"order",v=h[j]??d.default,f=(h[k]??d.defaultOrder??"asc")==="desc"?"desc":"asc";if(!v)return c;if(!d.allowed.includes(v))return c;return c.sort((b,w)=>{if(typeof b!=="object"||b===null)return 0;if(typeof w!=="object"||w===null)return 0;let z=b,D=w,M=z[v],T=D[v];if(M===T)return 0;if(M===void 0)return 1;if(T===void 0)return-1;let A;if(typeof M==="number"&&typeof T==="number")A=M-T;else A=String(M).localeCompare(String(T));return f==="desc"?-A:A})}function H(c,h,d){let j=d.pageParam??"page",k=d.limitParam??"limit",v=d.defaultLimit??10,g=d.maxLimit??100,f=Math.max(1,Number.parseInt(h[j]||"1",10)||1),b=Math.min(g,Math.max(1,Number.parseInt(h[k]||String(v),10)||v)),w=c.length,z=Math.ceil(w/b),D=(f-1)*b;return{data:c.slice(D,D+b),pagination:{page:f,limit:b,total:w,totalPages:z}}}export{N as queryPlugin};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schmock/query",
|
|
3
3
|
"description": "Pagination, filtering, and sorting plugin for Schmock",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "2.0.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@schmock/core": "^
|
|
33
|
+
"@schmock/core": "^2.0.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@amiceli/vitest-cucumber": "^6.3.0",
|
package/src/index.test.ts
CHANGED
|
@@ -5,7 +5,7 @@ describe("queryPlugin", () => {
|
|
|
5
5
|
it("creates a plugin with correct name", () => {
|
|
6
6
|
const plugin = queryPlugin({ pagination: {} });
|
|
7
7
|
expect(plugin.name).toBe("query");
|
|
8
|
-
expect(plugin.version).toBe("
|
|
8
|
+
expect(plugin.version).toBe("2.0.1");
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it("passes through non-array responses", async () => {
|
|
@@ -131,4 +131,334 @@ describe("queryPlugin", () => {
|
|
|
131
131
|
|
|
132
132
|
expect(result.response.data).toHaveLength(3);
|
|
133
133
|
});
|
|
134
|
+
|
|
135
|
+
describe("filtering edge cases", () => {
|
|
136
|
+
it("returns empty result when input array is empty", async () => {
|
|
137
|
+
const plugin = queryPlugin({
|
|
138
|
+
filtering: { allowed: ["role"] },
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const result = await plugin.process(
|
|
142
|
+
{
|
|
143
|
+
path: "/test",
|
|
144
|
+
route: {},
|
|
145
|
+
method: "GET",
|
|
146
|
+
params: {},
|
|
147
|
+
query: { "filter[role]": "admin" },
|
|
148
|
+
headers: {},
|
|
149
|
+
state: new Map(),
|
|
150
|
+
},
|
|
151
|
+
[],
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
expect(result.response).toEqual([]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("excludes items that lack the filter field", async () => {
|
|
158
|
+
const plugin = queryPlugin({
|
|
159
|
+
filtering: { allowed: ["role"] },
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const items = [
|
|
163
|
+
{ id: 1, role: "admin" },
|
|
164
|
+
{ id: 2, name: "no-role" },
|
|
165
|
+
{ id: 3, role: "admin" },
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
const result = await plugin.process(
|
|
169
|
+
{
|
|
170
|
+
path: "/test",
|
|
171
|
+
route: {},
|
|
172
|
+
method: "GET",
|
|
173
|
+
params: {},
|
|
174
|
+
query: { "filter[role]": "admin" },
|
|
175
|
+
headers: {},
|
|
176
|
+
state: new Map(),
|
|
177
|
+
},
|
|
178
|
+
items,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
expect(result.response).toHaveLength(2);
|
|
182
|
+
expect(result.response.every((i: any) => i.role === "admin")).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("handles filter value with special regex characters as literal string", async () => {
|
|
186
|
+
const plugin = queryPlugin({
|
|
187
|
+
filtering: { allowed: ["domain"] },
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const items = [
|
|
191
|
+
{ id: 1, domain: "foo.bar" },
|
|
192
|
+
{ id: 2, domain: "fooXbar" },
|
|
193
|
+
{ id: 3, domain: "foo.bar" },
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
const result = await plugin.process(
|
|
197
|
+
{
|
|
198
|
+
path: "/test",
|
|
199
|
+
route: {},
|
|
200
|
+
method: "GET",
|
|
201
|
+
params: {},
|
|
202
|
+
query: { "filter[domain]": "foo.bar" },
|
|
203
|
+
headers: {},
|
|
204
|
+
state: new Map(),
|
|
205
|
+
},
|
|
206
|
+
items,
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
expect(result.response).toHaveLength(2);
|
|
210
|
+
expect(result.response.every((i: any) => i.domain === "foo.bar")).toBe(
|
|
211
|
+
true,
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe("sorting edge cases", () => {
|
|
217
|
+
it("sorts with mixed numeric and string values in the sort field", async () => {
|
|
218
|
+
const plugin = queryPlugin({
|
|
219
|
+
sorting: { allowed: ["score"], default: "score" },
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const items = [
|
|
223
|
+
{ id: 1, score: 10 },
|
|
224
|
+
{ id: 2, score: "5" },
|
|
225
|
+
{ id: 3, score: 2 },
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
const result = await plugin.process(
|
|
229
|
+
{
|
|
230
|
+
path: "/test",
|
|
231
|
+
route: {},
|
|
232
|
+
method: "GET",
|
|
233
|
+
params: {},
|
|
234
|
+
query: { sort: "score", order: "asc" },
|
|
235
|
+
headers: {},
|
|
236
|
+
state: new Map(),
|
|
237
|
+
},
|
|
238
|
+
items,
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// Mixed types fall back to localeCompare via String() coercion
|
|
242
|
+
expect(result.response).toHaveLength(3);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("pushes items with undefined sort field values to the end", async () => {
|
|
246
|
+
const plugin = queryPlugin({
|
|
247
|
+
sorting: { allowed: ["name"], default: "name" },
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const items = [
|
|
251
|
+
{ id: 1 },
|
|
252
|
+
{ id: 2, name: "Alice" },
|
|
253
|
+
{ id: 3, name: "Bob" },
|
|
254
|
+
];
|
|
255
|
+
|
|
256
|
+
const result = await plugin.process(
|
|
257
|
+
{
|
|
258
|
+
path: "/test",
|
|
259
|
+
route: {},
|
|
260
|
+
method: "GET",
|
|
261
|
+
params: {},
|
|
262
|
+
query: { sort: "name", order: "asc" },
|
|
263
|
+
headers: {},
|
|
264
|
+
state: new Map(),
|
|
265
|
+
},
|
|
266
|
+
items,
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
expect(result.response[0].name).toBe("Alice");
|
|
270
|
+
expect(result.response[1].name).toBe("Bob");
|
|
271
|
+
expect(result.response[2].name).toBeUndefined();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("maintains relative order for items with equal sort values", async () => {
|
|
275
|
+
const plugin = queryPlugin({
|
|
276
|
+
sorting: { allowed: ["priority"], default: "priority" },
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const items = [
|
|
280
|
+
{ id: 1, priority: 1, label: "first" },
|
|
281
|
+
{ id: 2, priority: 1, label: "second" },
|
|
282
|
+
{ id: 3, priority: 1, label: "third" },
|
|
283
|
+
];
|
|
284
|
+
|
|
285
|
+
const result = await plugin.process(
|
|
286
|
+
{
|
|
287
|
+
path: "/test",
|
|
288
|
+
route: {},
|
|
289
|
+
method: "GET",
|
|
290
|
+
params: {},
|
|
291
|
+
query: { sort: "priority", order: "asc" },
|
|
292
|
+
headers: {},
|
|
293
|
+
state: new Map(),
|
|
294
|
+
},
|
|
295
|
+
items,
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
// Sort stability: equal values preserve insertion order
|
|
299
|
+
expect(result.response.map((i: any) => i.label)).toEqual([
|
|
300
|
+
"first",
|
|
301
|
+
"second",
|
|
302
|
+
"third",
|
|
303
|
+
]);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe("pagination edge cases", () => {
|
|
308
|
+
const makePaginationPlugin = (defaultLimit = 10, maxLimit = 100) =>
|
|
309
|
+
queryPlugin({
|
|
310
|
+
pagination: { defaultLimit, maxLimit },
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const items = Array.from({ length: 5 }, (_, i) => ({ id: i + 1 }));
|
|
314
|
+
|
|
315
|
+
const makeContext = (query: Record<string, string>) => ({
|
|
316
|
+
path: "/test",
|
|
317
|
+
route: {},
|
|
318
|
+
method: "GET" as const,
|
|
319
|
+
params: {},
|
|
320
|
+
query,
|
|
321
|
+
headers: {},
|
|
322
|
+
state: new Map(),
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("clamps page=0 to page 1", async () => {
|
|
326
|
+
const plugin = makePaginationPlugin(2);
|
|
327
|
+
const result = await plugin.process(makeContext({ page: "0" }), items);
|
|
328
|
+
|
|
329
|
+
expect(result.response.pagination.page).toBe(1);
|
|
330
|
+
expect(result.response.data).toEqual([{ id: 1 }, { id: 2 }]);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("clamps page=-1 to page 1", async () => {
|
|
334
|
+
const plugin = makePaginationPlugin(2);
|
|
335
|
+
const result = await plugin.process(makeContext({ page: "-1" }), items);
|
|
336
|
+
|
|
337
|
+
expect(result.response.pagination.page).toBe(1);
|
|
338
|
+
expect(result.response.data).toEqual([{ id: 1 }, { id: 2 }]);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("clamps limit=0 to defaultLimit (0 is falsy, falls back to default)", async () => {
|
|
342
|
+
const plugin = makePaginationPlugin(10, 100);
|
|
343
|
+
const result = await plugin.process(makeContext({ limit: "0" }), items);
|
|
344
|
+
|
|
345
|
+
// parseInt("0") is 0, which is falsy → falls back to defaultLimit (10)
|
|
346
|
+
expect(result.response.pagination.limit).toBe(10);
|
|
347
|
+
expect(result.response.data).toHaveLength(5);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("clamps limit exceeding maxLimit to maxLimit", async () => {
|
|
351
|
+
const plugin = makePaginationPlugin(10, 3);
|
|
352
|
+
const result = await plugin.process(makeContext({ limit: "999" }), items);
|
|
353
|
+
|
|
354
|
+
expect(result.response.pagination.limit).toBe(3);
|
|
355
|
+
expect(result.response.data).toHaveLength(3);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("defaults gracefully when page is non-numeric", async () => {
|
|
359
|
+
const plugin = makePaginationPlugin(2);
|
|
360
|
+
const result = await plugin.process(makeContext({ page: "abc" }), items);
|
|
361
|
+
|
|
362
|
+
expect(result.response.pagination.page).toBe(1);
|
|
363
|
+
expect(result.response.data).toEqual([{ id: 1 }, { id: 2 }]);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("defaults gracefully when limit is non-numeric", async () => {
|
|
367
|
+
const plugin = makePaginationPlugin(2);
|
|
368
|
+
const result = await plugin.process(makeContext({ limit: "abc" }), items);
|
|
369
|
+
|
|
370
|
+
expect(result.response.pagination.limit).toBe(2);
|
|
371
|
+
expect(result.response.data).toEqual([{ id: 1 }, { id: 2 }]);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("returns empty data with correct total for a very large page number", async () => {
|
|
375
|
+
const plugin = makePaginationPlugin(2);
|
|
376
|
+
const result = await plugin.process(makeContext({ page: "9999" }), items);
|
|
377
|
+
|
|
378
|
+
expect(result.response.data).toEqual([]);
|
|
379
|
+
expect(result.response.pagination.total).toBe(5);
|
|
380
|
+
expect(result.response.pagination.totalPages).toBe(3);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("returns data=[], total=0, totalPages=0 for empty array input", async () => {
|
|
384
|
+
const plugin = makePaginationPlugin(10);
|
|
385
|
+
const result = await plugin.process(makeContext({}), []);
|
|
386
|
+
|
|
387
|
+
expect(result.response.data).toEqual([]);
|
|
388
|
+
expect(result.response.pagination.total).toBe(0);
|
|
389
|
+
expect(result.response.pagination.totalPages).toBe(0);
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
describe("process method edge cases", () => {
|
|
394
|
+
it("passes through null response unchanged", async () => {
|
|
395
|
+
const plugin = queryPlugin({
|
|
396
|
+
pagination: { defaultLimit: 10 },
|
|
397
|
+
filtering: { allowed: ["role"] },
|
|
398
|
+
sorting: { allowed: ["name"] },
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
const result = await plugin.process(
|
|
402
|
+
{
|
|
403
|
+
path: "/test",
|
|
404
|
+
route: {},
|
|
405
|
+
method: "GET",
|
|
406
|
+
params: {},
|
|
407
|
+
query: {},
|
|
408
|
+
headers: {},
|
|
409
|
+
state: new Map(),
|
|
410
|
+
},
|
|
411
|
+
null,
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
expect(result.response).toBeNull();
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("passes through object (non-array) response unchanged", async () => {
|
|
418
|
+
const plugin = queryPlugin({
|
|
419
|
+
pagination: { defaultLimit: 10 },
|
|
420
|
+
filtering: { allowed: ["role"] },
|
|
421
|
+
sorting: { allowed: ["name"] },
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
const obj = { message: "hello", count: 42 };
|
|
425
|
+
const result = await plugin.process(
|
|
426
|
+
{
|
|
427
|
+
path: "/test",
|
|
428
|
+
route: {},
|
|
429
|
+
method: "GET",
|
|
430
|
+
params: {},
|
|
431
|
+
query: {},
|
|
432
|
+
headers: {},
|
|
433
|
+
state: new Map(),
|
|
434
|
+
},
|
|
435
|
+
obj,
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
expect(result.response).toEqual(obj);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it("passes through undefined response unchanged", async () => {
|
|
442
|
+
const plugin = queryPlugin({
|
|
443
|
+
pagination: { defaultLimit: 10 },
|
|
444
|
+
filtering: { allowed: ["role"] },
|
|
445
|
+
sorting: { allowed: ["name"] },
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
const result = await plugin.process(
|
|
449
|
+
{
|
|
450
|
+
path: "/test",
|
|
451
|
+
route: {},
|
|
452
|
+
method: "GET",
|
|
453
|
+
params: {},
|
|
454
|
+
query: {},
|
|
455
|
+
headers: {},
|
|
456
|
+
state: new Map(),
|
|
457
|
+
},
|
|
458
|
+
undefined,
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
expect(result.response).toBeUndefined();
|
|
462
|
+
});
|
|
463
|
+
});
|
|
134
464
|
});
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/// <reference path="../../core/schmock.d.ts" />
|
|
2
2
|
|
|
3
|
+
import { version as packageVersion } from "../package.json";
|
|
4
|
+
|
|
3
5
|
interface PaginationOptions {
|
|
4
6
|
/** Default items per page (default: 10) */
|
|
5
7
|
defaultLimit?: number;
|
|
@@ -40,18 +42,18 @@ interface QueryPluginOptions {
|
|
|
40
42
|
export function queryPlugin(options: QueryPluginOptions): Schmock.Plugin {
|
|
41
43
|
return {
|
|
42
44
|
name: "query",
|
|
43
|
-
version:
|
|
45
|
+
version: packageVersion,
|
|
44
46
|
|
|
45
47
|
process(
|
|
46
48
|
context: Schmock.PluginContext,
|
|
47
|
-
response?:
|
|
49
|
+
response?: unknown,
|
|
48
50
|
): Schmock.PluginResult {
|
|
49
51
|
// Only process array responses
|
|
50
52
|
if (!Array.isArray(response)) {
|
|
51
53
|
return { context, response };
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
let items = [...response];
|
|
56
|
+
let items: unknown[] = [...response];
|
|
55
57
|
const query = context.query || {};
|
|
56
58
|
|
|
57
59
|
// Apply filtering
|
|
@@ -76,10 +78,10 @@ export function queryPlugin(options: QueryPluginOptions): Schmock.Plugin {
|
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
function applyFiltering(
|
|
79
|
-
items:
|
|
81
|
+
items: unknown[],
|
|
80
82
|
query: Record<string, string>,
|
|
81
83
|
options: FilteringOptions,
|
|
82
|
-
):
|
|
84
|
+
): unknown[] {
|
|
83
85
|
const prefix = options.filterPrefix ?? "filter";
|
|
84
86
|
let result = items;
|
|
85
87
|
|
|
@@ -93,7 +95,9 @@ function applyFiltering(
|
|
|
93
95
|
|
|
94
96
|
if (value !== undefined) {
|
|
95
97
|
result = result.filter((item) => {
|
|
96
|
-
|
|
98
|
+
if (typeof item !== "object" || item === null) return false;
|
|
99
|
+
const record = item as Record<string, unknown>;
|
|
100
|
+
const itemValue = record[field];
|
|
97
101
|
if (itemValue === undefined) return false;
|
|
98
102
|
// Intentional string coercion: query params are inherently strings
|
|
99
103
|
return String(itemValue) === value;
|
|
@@ -105,10 +109,10 @@ function applyFiltering(
|
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
function applySorting(
|
|
108
|
-
items:
|
|
112
|
+
items: unknown[],
|
|
109
113
|
query: Record<string, string>,
|
|
110
114
|
options: SortingOptions,
|
|
111
|
-
):
|
|
115
|
+
): unknown[] {
|
|
112
116
|
const sortParam = options.sortParam ?? "sort";
|
|
113
117
|
const orderParam = options.orderParam ?? "order";
|
|
114
118
|
const sortField = query[sortParam] ?? options.default;
|
|
@@ -121,8 +125,12 @@ function applySorting(
|
|
|
121
125
|
if (!options.allowed.includes(sortField)) return items;
|
|
122
126
|
|
|
123
127
|
return items.sort((a, b) => {
|
|
124
|
-
|
|
125
|
-
|
|
128
|
+
if (typeof a !== "object" || a === null) return 0;
|
|
129
|
+
if (typeof b !== "object" || b === null) return 0;
|
|
130
|
+
const aRecord = a as Record<string, unknown>;
|
|
131
|
+
const bRecord = b as Record<string, unknown>;
|
|
132
|
+
const aVal = aRecord[sortField];
|
|
133
|
+
const bVal = bRecord[sortField];
|
|
126
134
|
|
|
127
135
|
if (aVal === bVal) return 0;
|
|
128
136
|
if (aVal === undefined) return 1;
|
|
@@ -140,7 +148,7 @@ function applySorting(
|
|
|
140
148
|
}
|
|
141
149
|
|
|
142
150
|
interface PaginatedResult {
|
|
143
|
-
data:
|
|
151
|
+
data: unknown[];
|
|
144
152
|
pagination: {
|
|
145
153
|
page: number;
|
|
146
154
|
limit: number;
|
|
@@ -150,7 +158,7 @@ interface PaginatedResult {
|
|
|
150
158
|
}
|
|
151
159
|
|
|
152
160
|
function applyPagination(
|
|
153
|
-
items:
|
|
161
|
+
items: unknown[],
|
|
154
162
|
query: Record<string, string>,
|
|
155
163
|
options: PaginationOptions,
|
|
156
164
|
): PaginatedResult {
|
|
@@ -79,30 +79,6 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
79
79
|
});
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
-
Scenario("Pagination beyond last page returns empty", ({ Given, When, Then, And }) => {
|
|
83
|
-
Given("I create a mock with 25 items and pagination plugin", () => {
|
|
84
|
-
mock = schmock();
|
|
85
|
-
mock("GET /items", generateItems(25))
|
|
86
|
-
.pipe(queryPlugin({
|
|
87
|
-
pagination: { defaultLimit: 10, maxLimit: 100 },
|
|
88
|
-
}));
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
When("I request page 10", async () => {
|
|
92
|
-
response = await mock.handle("GET", "/items", {
|
|
93
|
-
query: { page: "10" },
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
Then("I should receive {int} items", (_, count: number) => {
|
|
98
|
-
expect(response.body.data).toHaveLength(count);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
And("the pagination total should be {int}", (_, total: number) => {
|
|
102
|
-
expect(response.body.pagination.total).toBe(total);
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
82
|
Scenario("Sort items ascending by name", ({ Given, When, Then, And }) => {
|
|
107
83
|
Given("I create a mock with named items and sorting plugin", () => {
|
|
108
84
|
mock = schmock();
|