@schmock/query 2.3.0 → 2.3.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/LICENSE +21 -0
- package/README.md +41 -0
- package/dist/index.d.ts +15 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +14 -5
- package/src/index.test.ts +0 -591
- package/src/index.ts +0 -232
- package/src/steps/query-plugin.steps.ts +0 -362
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Khalic Lab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# @schmock/query
|
|
2
|
+
|
|
3
|
+
Pagination, sorting, and filtering for Schmock list endpoints, driven by query parameters.
|
|
4
|
+
|
|
5
|
+
Part of [Schmock](https://github.com/khalic-lab/schmock) — mock APIs from OpenAPI specs or hand-crafted routes.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun add -d @schmock/query
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { schmock } from "@schmock/core";
|
|
17
|
+
import { queryPlugin } from "@schmock/query";
|
|
18
|
+
|
|
19
|
+
const mock = schmock();
|
|
20
|
+
|
|
21
|
+
mock("GET /users", users).pipe(
|
|
22
|
+
queryPlugin({
|
|
23
|
+
pagination: { defaultLimit: 10, maxLimit: 100 },
|
|
24
|
+
sorting: { allowed: ["name"] },
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// Through an adapter: GET /users?page=2&limit=10&sort=name
|
|
29
|
+
const response = await mock.handle("GET", "/users", {
|
|
30
|
+
query: { page: "2", limit: "10", sort: "name" },
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Documentation
|
|
35
|
+
|
|
36
|
+
- [API reference](https://github.com/khalic-lab/schmock/blob/main/docs/api.md)
|
|
37
|
+
- [Plugin development](https://github.com/khalic-lab/schmock/blob/main/docs/plugins.md)
|
|
38
|
+
|
|
39
|
+
## License
|
|
40
|
+
|
|
41
|
+
MIT © Khalic Lab
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import type * as Schmock from "@schmock/core";
|
|
2
|
+
export interface PaginationOptions {
|
|
2
3
|
/** Default items per page (default: 10) */
|
|
3
4
|
defaultLimit?: number;
|
|
4
5
|
/** Maximum items per page (default: 100) */
|
|
@@ -8,7 +9,7 @@ interface PaginationOptions {
|
|
|
8
9
|
/** Query parameter name for limit (default: "limit") */
|
|
9
10
|
limitParam?: string;
|
|
10
11
|
}
|
|
11
|
-
interface SortingOptions {
|
|
12
|
+
export interface SortingOptions {
|
|
12
13
|
/** Fields allowed for sorting */
|
|
13
14
|
allowed: string[];
|
|
14
15
|
/** Default sort field */
|
|
@@ -20,17 +21,25 @@ interface SortingOptions {
|
|
|
20
21
|
/** Query parameter name for sort order (default: "order") */
|
|
21
22
|
orderParam?: string;
|
|
22
23
|
}
|
|
23
|
-
interface FilteringOptions {
|
|
24
|
+
export interface FilteringOptions {
|
|
24
25
|
/** Fields allowed for filtering */
|
|
25
26
|
allowed: string[];
|
|
26
27
|
/** Query parameter prefix for filters (default: "filter") */
|
|
27
28
|
filterPrefix?: string;
|
|
28
29
|
}
|
|
29
|
-
interface QueryPluginOptions {
|
|
30
|
+
export interface QueryPluginOptions {
|
|
30
31
|
pagination?: PaginationOptions;
|
|
31
32
|
sorting?: SortingOptions;
|
|
32
33
|
filtering?: FilteringOptions;
|
|
33
34
|
}
|
|
34
|
-
export declare function queryPlugin(options
|
|
35
|
-
export {
|
|
35
|
+
export declare function queryPlugin(options?: QueryPluginOptions): Schmock.Plugin;
|
|
36
|
+
export interface PaginatedResult {
|
|
37
|
+
data: unknown[];
|
|
38
|
+
pagination: {
|
|
39
|
+
page: number;
|
|
40
|
+
limit: number;
|
|
41
|
+
total: number;
|
|
42
|
+
totalPages: number;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
36
45
|
//# sourceMappingURL=index.d.ts.map
|
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":"AAAA,OAAO,KAAK,KAAK,OAAO,MAAM,eAAe,CAAC;AAI9C,MAAM,WAAW,iBAAiB;IAChC,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,MAAM,WAAW,cAAc;IAC7B,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,MAAM,WAAW,gBAAgB;IAC/B,mCAAmC;IACnC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,6DAA6D;IAC7D,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,kBAAkB;IACjC,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,SAAS,CAAC,EAAE,gBAAgB,CAAC;CAC9B;AA6LD,wBAAgB,WAAW,CAAC,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,MAAM,CA2C5E;AA4JD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,OAAO,EAAE,CAAC;IAChB,UAAU,EAAE;QACV,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;CACH"}
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{isStatusTuple as
|
|
1
|
+
import{isStatusTuple as L,SchmockError as F}from"@schmock/core";var _="2.3.1";var c=new Set(["__proto__","constructor","prototype"]);function T(h){return typeof h==="object"&&h!==null}function W(h,j){try{return Object.hasOwn(h,j)?h[j]:void 0}catch{return}}function O(h){try{return typeof h==="string"?JSON.stringify(h):String(h)}catch{return"<unprintable>"}}function Z(h,j,M){return new F(`queryPlugin: ${h} (received ${O(M)})`,"QUERY_CONFIG_INVALID",{option:j,received:M})}function I(h,j){if(h===void 0)return;if(typeof h!=="number"||!Number.isSafeInteger(h)||h<1)throw Z(`${j} must be a positive integer`,j,h)}function Y(h,j){if(h===void 0)return;if(typeof h!=="string"||h.length===0)throw Z(`${j} must be a non-empty string`,j,h)}function K(h,j){if(!Array.isArray(h))throw Z(`${j} must be an array of field names`,j,h);for(let M of h){if(typeof M!=="string"||M.length===0)throw Z(`${j} must contain only non-empty field names`,j,M);if(c.has(M))throw Z(`${j} must not contain the reserved field name`,j,M)}}function V(h){let{pagination:j,sorting:M,filtering:z}=h;if(j)I(j.defaultLimit,"pagination.defaultLimit"),I(j.maxLimit,"pagination.maxLimit"),Y(j.pageParam,"pagination.pageParam"),Y(j.limitParam,"pagination.limitParam");if(M)K(M.allowed,"sorting.allowed"),Y(M.sortParam,"sorting.sortParam"),Y(M.orderParam,"sorting.orderParam");if(z)K(z.allowed,"filtering.allowed"),Y(z.filterPrefix,"filtering.filterPrefix")}function q(h){let{pagination:j,sorting:M,filtering:z}=h,D,H,Q;if(j){let{defaultLimit:J,maxLimit:G,pageParam:U,limitParam:X}=j;D={defaultLimit:J,maxLimit:G,pageParam:U,limitParam:X}}if(M){let{allowed:J,default:G,defaultOrder:U,sortParam:X,orderParam:$}=M;H={allowed:Array.isArray(J)?[...J]:J,default:G,defaultOrder:U,sortParam:X,orderParam:$}}if(z){let{allowed:J,filterPrefix:G}=z;Q={allowed:Array.isArray(J)?[...J]:J,filterPrefix:G}}return{pagination:D,sorting:H,filtering:Q}}function P(h){return T(h)&&!Array.isArray(h)&&Object.values(h).every((j)=>typeof j==="string")}function N(h){return T(h)&&!Array.isArray(h)&&typeof h.status==="number"&&"body"in h&&(h.headers===void 0||P(h.headers))}function E(h){if(L(h))return h[1];if(N(h))return h.body;return h}function k(h,j){if(L(h))return h.length===3?[h[0],j,h[2]]:[h[0],j];if(N(h))return h.headers===void 0?{status:h.status,body:j}:{status:h.status,body:j,headers:h.headers};return j}function u(h={}){let j=q(h);return V(j),{name:"query",version:_,process(M,z){let D=E(z);if(!Array.isArray(D))return{context:M,response:z};let H=[...D],Q=M.query||{};if(j.filtering)H=R(H,Q,j.filtering);if(j.sorting)H=b(H,Q,j.sorting);if(j.pagination){let J=d(H,Q,j.pagination);return{context:M,response:k(z,J)}}return{context:M,response:k(z,H)}}}}function R(h,j,M){let z=M.filterPrefix??"filter",D=h;for(let H of M.allowed){let Q=W(j,`${z}[${H}]`)??W(j,`${z}.${H}`);if(Q!==void 0)D=D.filter((J)=>{if(!T(J))return!1;let G=W(J,H);if(G===void 0)return!1;try{return String(G)===Q}catch{return!1}})}return D}function w(h,j){if(h<j)return-1;return h>j?1:0}function S(h){if(h===void 0)return{kind:"missing"};if(h===null)return{kind:"null"};if(typeof h==="number")return Number.isFinite(h)?{kind:"number",value:h}:{kind:"non-finite-number",value:h};if(typeof h==="string")return{kind:"string",value:h};if(typeof h==="boolean")return{kind:"boolean",value:h};if(typeof h==="bigint")return{kind:"bigint",value:h};return{kind:"unsupported"}}function A(h){switch(h.kind){case"number":return 0;case"non-finite-number":return 1;case"string":return 2;case"boolean":return 3;case"bigint":return 4;case"null":return 5;case"unsupported":return 6;case"missing":return 7}}function C(h){if(h===Number.NEGATIVE_INFINITY)return 0;if(h===Number.POSITIVE_INFINITY)return 1;return 2}function f(h,j,M){if(h.kind==="missing"||j.kind==="missing"){if(h.kind===j.kind)return 0;return h.kind==="missing"?1:-1}if(h.kind==="unsupported"||j.kind==="unsupported"){if(h.kind===j.kind)return 0;return h.kind==="unsupported"?1:-1}let z=A(h)-A(j),D=z;if(z===0){if(h.kind==="number"&&j.kind==="number")D=h.value<j.value?-1:h.value>j.value?1:0;else if(h.kind==="non-finite-number"&&j.kind==="non-finite-number")D=C(h.value)-C(j.value);else if(h.kind==="string"&&j.kind==="string")D=w(h.value,j.value);else if(h.kind==="boolean"&&j.kind==="boolean")D=Number(h.value)-Number(j.value);else if(h.kind==="bigint"&&j.kind==="bigint")D=h.value<j.value?-1:h.value>j.value?1:0}return M==="desc"?-D:D}function b(h,j,M){let z=M.sortParam??"sort",D=M.orderParam??"order",H=W(j,z)??M.default,J=(W(j,D)??M.defaultOrder??"asc")==="desc"?"desc":"asc";if(!H)return h;if(!M.allowed.includes(H))return h;return h.map((G)=>({item:G,key:S(T(G)?W(G,H):void 0)})).sort((G,U)=>f(G.key,U.key,J)).map(({item:G})=>G)}function B(h,j){if(typeof h!=="string"||!/^[0-9]+$/.test(h))return j;let M=Number(h);return Number.isSafeInteger(M)&&M>=1?M:j}function d(h,j,M){let z=M.pageParam??"page",D=M.limitParam??"limit",H=M.defaultLimit??10,Q=M.maxLimit??100,J=B(W(j,z),1),G=Math.min(Q,B(W(j,D),H)),U=h.length,X=Math.ceil(U/G),$=(J-1)*G;return{data:h.slice($,$+G),pagination:{page:J,limit:G,total:U,totalPages:X}}}export{u as queryPlugin};
|
package/package.json
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schmock/query",
|
|
3
3
|
"description": "Pagination, filtering, and sorting plugin for Schmock",
|
|
4
|
-
"version": "2.3.
|
|
4
|
+
"version": "2.3.1",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/khalic-lab/schmock.git",
|
|
9
|
+
"directory": "packages/query"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/khalic-lab/schmock/tree/main/packages/query#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/khalic-lab/schmock/issues"
|
|
14
|
+
},
|
|
6
15
|
"main": "./dist/index.js",
|
|
7
16
|
"types": "./dist/index.d.ts",
|
|
8
17
|
"files": [
|
|
9
|
-
"dist"
|
|
10
|
-
"src"
|
|
18
|
+
"dist"
|
|
11
19
|
],
|
|
12
20
|
"exports": {
|
|
13
21
|
".": {
|
|
@@ -18,7 +26,8 @@
|
|
|
18
26
|
"./package.json": "./package.json"
|
|
19
27
|
},
|
|
20
28
|
"scripts": {
|
|
21
|
-
"
|
|
29
|
+
"clean": "rm -rf dist && rm -f tsconfig.tsbuildinfo",
|
|
30
|
+
"build": "bun run clean && bun run build:lib && bun run build:types",
|
|
22
31
|
"build:lib": "bun build --minify --target node --format esm --outdir=dist src/index.ts --external @schmock/core",
|
|
23
32
|
"build:types": "tsc -p tsconfig.json",
|
|
24
33
|
"test": "vitest run",
|
|
@@ -30,7 +39,7 @@
|
|
|
30
39
|
},
|
|
31
40
|
"license": "MIT",
|
|
32
41
|
"peerDependencies": {
|
|
33
|
-
"@schmock/core": "^2.3.
|
|
42
|
+
"@schmock/core": "^2.3.1"
|
|
34
43
|
},
|
|
35
44
|
"devDependencies": {
|
|
36
45
|
"@amiceli/vitest-cucumber": "^7.0.0",
|
package/src/index.test.ts
DELETED
|
@@ -1,591 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { version as packageVersion } from "../package.json";
|
|
3
|
-
import { queryPlugin } from "./index";
|
|
4
|
-
|
|
5
|
-
describe("queryPlugin", () => {
|
|
6
|
-
it("creates a plugin with correct name", () => {
|
|
7
|
-
const plugin = queryPlugin({ pagination: {} });
|
|
8
|
-
expect(plugin.name).toBe("query");
|
|
9
|
-
expect(plugin.version).toBe(packageVersion);
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it("passes through non-array responses", async () => {
|
|
13
|
-
const plugin = queryPlugin({ pagination: {} });
|
|
14
|
-
const result = await plugin.process(
|
|
15
|
-
{
|
|
16
|
-
path: "/test",
|
|
17
|
-
route: {},
|
|
18
|
-
method: "GET",
|
|
19
|
-
params: {},
|
|
20
|
-
query: {},
|
|
21
|
-
headers: {},
|
|
22
|
-
state: new Map(),
|
|
23
|
-
},
|
|
24
|
-
{ message: "not an array" },
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
expect(result.response).toEqual({ message: "not an array" });
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("paginates array responses", async () => {
|
|
31
|
-
const plugin = queryPlugin({
|
|
32
|
-
pagination: { defaultLimit: 2, maxLimit: 10 },
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
const items = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }];
|
|
36
|
-
const result = await plugin.process(
|
|
37
|
-
{
|
|
38
|
-
path: "/test",
|
|
39
|
-
route: {},
|
|
40
|
-
method: "GET",
|
|
41
|
-
params: {},
|
|
42
|
-
query: { page: "2", limit: "2" },
|
|
43
|
-
headers: {},
|
|
44
|
-
state: new Map(),
|
|
45
|
-
},
|
|
46
|
-
items,
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
expect(result.response).toEqual({
|
|
50
|
-
data: [{ id: 3 }, { id: 4 }],
|
|
51
|
-
pagination: { page: 2, limit: 2, total: 5, totalPages: 3 },
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("paginates the array body of a two-element status tuple", async () => {
|
|
56
|
-
const plugin = queryPlugin({
|
|
57
|
-
pagination: { defaultLimit: 2, maxLimit: 10 },
|
|
58
|
-
});
|
|
59
|
-
const items = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }];
|
|
60
|
-
const context: Schmock.PluginContext = {
|
|
61
|
-
path: "/test",
|
|
62
|
-
route: {},
|
|
63
|
-
method: "GET",
|
|
64
|
-
params: {},
|
|
65
|
-
query: { page: "2", limit: "2" },
|
|
66
|
-
headers: {},
|
|
67
|
-
state: new Map(),
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const result = await plugin.process(context, [206, items] satisfies [
|
|
71
|
-
number,
|
|
72
|
-
unknown,
|
|
73
|
-
]);
|
|
74
|
-
|
|
75
|
-
expect(result.response).toEqual([
|
|
76
|
-
206,
|
|
77
|
-
{
|
|
78
|
-
data: [{ id: 3 }, { id: 4 }],
|
|
79
|
-
pagination: { page: 2, limit: 2, total: 5, totalPages: 3 },
|
|
80
|
-
},
|
|
81
|
-
]);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("paginates a status tuple body without changing its headers", async () => {
|
|
85
|
-
const plugin = queryPlugin({
|
|
86
|
-
pagination: { defaultLimit: 2, maxLimit: 10 },
|
|
87
|
-
});
|
|
88
|
-
const items = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }];
|
|
89
|
-
const headers = {
|
|
90
|
-
"content-type": "application/json",
|
|
91
|
-
"x-response-source": "tuple",
|
|
92
|
-
};
|
|
93
|
-
const context: Schmock.PluginContext = {
|
|
94
|
-
path: "/test",
|
|
95
|
-
route: {},
|
|
96
|
-
method: "GET",
|
|
97
|
-
params: {},
|
|
98
|
-
query: { page: "2", limit: "2" },
|
|
99
|
-
headers: {},
|
|
100
|
-
state: new Map(),
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const result = await plugin.process(context, [
|
|
104
|
-
206,
|
|
105
|
-
items,
|
|
106
|
-
headers,
|
|
107
|
-
] satisfies [number, unknown, Record<string, string>]);
|
|
108
|
-
|
|
109
|
-
expect(result.response).toEqual([
|
|
110
|
-
206,
|
|
111
|
-
{
|
|
112
|
-
data: [{ id: 3 }, { id: 4 }],
|
|
113
|
-
pagination: { page: 2, limit: 2, total: 5, totalPages: 3 },
|
|
114
|
-
},
|
|
115
|
-
headers,
|
|
116
|
-
]);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("paginates a structured response body without changing metadata", async () => {
|
|
120
|
-
const plugin = queryPlugin({
|
|
121
|
-
pagination: { defaultLimit: 2, maxLimit: 10 },
|
|
122
|
-
});
|
|
123
|
-
const items = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }];
|
|
124
|
-
const response = {
|
|
125
|
-
status: 206,
|
|
126
|
-
body: items,
|
|
127
|
-
headers: { "x-response-source": "structured" },
|
|
128
|
-
};
|
|
129
|
-
const context: Schmock.PluginContext = {
|
|
130
|
-
path: "/test",
|
|
131
|
-
route: {},
|
|
132
|
-
method: "GET",
|
|
133
|
-
params: {},
|
|
134
|
-
query: { page: "2", limit: "2" },
|
|
135
|
-
headers: {},
|
|
136
|
-
state: new Map(),
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
const result = await plugin.process(context, response);
|
|
140
|
-
|
|
141
|
-
expect(result.response).toEqual({
|
|
142
|
-
status: 206,
|
|
143
|
-
body: {
|
|
144
|
-
data: [{ id: 3 }, { id: 4 }],
|
|
145
|
-
pagination: { page: 2, limit: 2, total: 5, totalPages: 3 },
|
|
146
|
-
},
|
|
147
|
-
headers: { "x-response-source": "structured" },
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it("passes response containers through when their semantic body is not an array", async () => {
|
|
152
|
-
const plugin = queryPlugin({ pagination: { defaultLimit: 2 } });
|
|
153
|
-
const context: Schmock.PluginContext = {
|
|
154
|
-
path: "/test",
|
|
155
|
-
route: {},
|
|
156
|
-
method: "GET",
|
|
157
|
-
params: {},
|
|
158
|
-
query: { page: "2", limit: "2" },
|
|
159
|
-
headers: {},
|
|
160
|
-
state: new Map(),
|
|
161
|
-
};
|
|
162
|
-
const responses: unknown[] = [
|
|
163
|
-
[
|
|
164
|
-
202,
|
|
165
|
-
{ message: "accepted" },
|
|
166
|
-
{ "x-response-source": "tuple" },
|
|
167
|
-
] satisfies [number, unknown, Record<string, string>],
|
|
168
|
-
{
|
|
169
|
-
status: 202,
|
|
170
|
-
body: { message: "accepted" },
|
|
171
|
-
headers: { "x-response-source": "structured" },
|
|
172
|
-
},
|
|
173
|
-
];
|
|
174
|
-
|
|
175
|
-
for (const response of responses) {
|
|
176
|
-
const result = await plugin.process(context, response);
|
|
177
|
-
expect(result.response).toBe(response);
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it("sorts array responses", async () => {
|
|
182
|
-
const plugin = queryPlugin({
|
|
183
|
-
sorting: { allowed: ["name"], default: "name" },
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
const items = [
|
|
187
|
-
{ id: 3, name: "Charlie" },
|
|
188
|
-
{ id: 1, name: "Alice" },
|
|
189
|
-
{ id: 2, name: "Bob" },
|
|
190
|
-
];
|
|
191
|
-
|
|
192
|
-
const result = await plugin.process(
|
|
193
|
-
{
|
|
194
|
-
path: "/test",
|
|
195
|
-
route: {},
|
|
196
|
-
method: "GET",
|
|
197
|
-
params: {},
|
|
198
|
-
query: { sort: "name", order: "asc" },
|
|
199
|
-
headers: {},
|
|
200
|
-
state: new Map(),
|
|
201
|
-
},
|
|
202
|
-
items,
|
|
203
|
-
);
|
|
204
|
-
|
|
205
|
-
expect(result.response.map((i: any) => i.name)).toEqual([
|
|
206
|
-
"Alice",
|
|
207
|
-
"Bob",
|
|
208
|
-
"Charlie",
|
|
209
|
-
]);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it("filters array responses", async () => {
|
|
213
|
-
const plugin = queryPlugin({
|
|
214
|
-
filtering: { allowed: ["role"] },
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
const items = [
|
|
218
|
-
{ id: 1, name: "Alice", role: "admin" },
|
|
219
|
-
{ id: 2, name: "Bob", role: "user" },
|
|
220
|
-
{ id: 3, name: "Charlie", role: "admin" },
|
|
221
|
-
];
|
|
222
|
-
|
|
223
|
-
const result = await plugin.process(
|
|
224
|
-
{
|
|
225
|
-
path: "/test",
|
|
226
|
-
route: {},
|
|
227
|
-
method: "GET",
|
|
228
|
-
params: {},
|
|
229
|
-
query: { "filter[role]": "admin" },
|
|
230
|
-
headers: {},
|
|
231
|
-
state: new Map(),
|
|
232
|
-
},
|
|
233
|
-
items,
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
expect(result.response).toHaveLength(2);
|
|
237
|
-
expect(result.response.every((i: any) => i.role === "admin")).toBe(true);
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it("respects maxLimit", async () => {
|
|
241
|
-
const plugin = queryPlugin({
|
|
242
|
-
pagination: { defaultLimit: 5, maxLimit: 3 },
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
const items = Array.from({ length: 10 }, (_, i) => ({ id: i }));
|
|
246
|
-
const result = await plugin.process(
|
|
247
|
-
{
|
|
248
|
-
path: "/test",
|
|
249
|
-
route: {},
|
|
250
|
-
method: "GET",
|
|
251
|
-
params: {},
|
|
252
|
-
query: { limit: "100" },
|
|
253
|
-
headers: {},
|
|
254
|
-
state: new Map(),
|
|
255
|
-
},
|
|
256
|
-
items,
|
|
257
|
-
);
|
|
258
|
-
|
|
259
|
-
expect(result.response.data).toHaveLength(3);
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
describe("filtering edge cases", () => {
|
|
263
|
-
it("returns empty result when input array is empty", async () => {
|
|
264
|
-
const plugin = queryPlugin({
|
|
265
|
-
filtering: { allowed: ["role"] },
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
const result = await plugin.process(
|
|
269
|
-
{
|
|
270
|
-
path: "/test",
|
|
271
|
-
route: {},
|
|
272
|
-
method: "GET",
|
|
273
|
-
params: {},
|
|
274
|
-
query: { "filter[role]": "admin" },
|
|
275
|
-
headers: {},
|
|
276
|
-
state: new Map(),
|
|
277
|
-
},
|
|
278
|
-
[],
|
|
279
|
-
);
|
|
280
|
-
|
|
281
|
-
expect(result.response).toEqual([]);
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it("excludes items that lack the filter field", async () => {
|
|
285
|
-
const plugin = queryPlugin({
|
|
286
|
-
filtering: { allowed: ["role"] },
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
const items = [
|
|
290
|
-
{ id: 1, role: "admin" },
|
|
291
|
-
{ id: 2, name: "no-role" },
|
|
292
|
-
{ id: 3, role: "admin" },
|
|
293
|
-
];
|
|
294
|
-
|
|
295
|
-
const result = await plugin.process(
|
|
296
|
-
{
|
|
297
|
-
path: "/test",
|
|
298
|
-
route: {},
|
|
299
|
-
method: "GET",
|
|
300
|
-
params: {},
|
|
301
|
-
query: { "filter[role]": "admin" },
|
|
302
|
-
headers: {},
|
|
303
|
-
state: new Map(),
|
|
304
|
-
},
|
|
305
|
-
items,
|
|
306
|
-
);
|
|
307
|
-
|
|
308
|
-
expect(result.response).toHaveLength(2);
|
|
309
|
-
expect(result.response.every((i: any) => i.role === "admin")).toBe(true);
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it("handles filter value with special regex characters as literal string", async () => {
|
|
313
|
-
const plugin = queryPlugin({
|
|
314
|
-
filtering: { allowed: ["domain"] },
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
const items = [
|
|
318
|
-
{ id: 1, domain: "foo.bar" },
|
|
319
|
-
{ id: 2, domain: "fooXbar" },
|
|
320
|
-
{ id: 3, domain: "foo.bar" },
|
|
321
|
-
];
|
|
322
|
-
|
|
323
|
-
const result = await plugin.process(
|
|
324
|
-
{
|
|
325
|
-
path: "/test",
|
|
326
|
-
route: {},
|
|
327
|
-
method: "GET",
|
|
328
|
-
params: {},
|
|
329
|
-
query: { "filter[domain]": "foo.bar" },
|
|
330
|
-
headers: {},
|
|
331
|
-
state: new Map(),
|
|
332
|
-
},
|
|
333
|
-
items,
|
|
334
|
-
);
|
|
335
|
-
|
|
336
|
-
expect(result.response).toHaveLength(2);
|
|
337
|
-
expect(result.response.every((i: any) => i.domain === "foo.bar")).toBe(
|
|
338
|
-
true,
|
|
339
|
-
);
|
|
340
|
-
});
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
describe("sorting edge cases", () => {
|
|
344
|
-
it("sorts with mixed numeric and string values in the sort field", async () => {
|
|
345
|
-
const plugin = queryPlugin({
|
|
346
|
-
sorting: { allowed: ["score"], default: "score" },
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
const items = [
|
|
350
|
-
{ id: 1, score: 10 },
|
|
351
|
-
{ id: 2, score: "5" },
|
|
352
|
-
{ id: 3, score: 2 },
|
|
353
|
-
];
|
|
354
|
-
|
|
355
|
-
const result = await plugin.process(
|
|
356
|
-
{
|
|
357
|
-
path: "/test",
|
|
358
|
-
route: {},
|
|
359
|
-
method: "GET",
|
|
360
|
-
params: {},
|
|
361
|
-
query: { sort: "score", order: "asc" },
|
|
362
|
-
headers: {},
|
|
363
|
-
state: new Map(),
|
|
364
|
-
},
|
|
365
|
-
items,
|
|
366
|
-
);
|
|
367
|
-
|
|
368
|
-
// Mixed types fall back to localeCompare via String() coercion
|
|
369
|
-
expect(result.response).toHaveLength(3);
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
it("pushes items with undefined sort field values to the end", async () => {
|
|
373
|
-
const plugin = queryPlugin({
|
|
374
|
-
sorting: { allowed: ["name"], default: "name" },
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
const items = [
|
|
378
|
-
{ id: 1 },
|
|
379
|
-
{ id: 2, name: "Alice" },
|
|
380
|
-
{ id: 3, name: "Bob" },
|
|
381
|
-
];
|
|
382
|
-
|
|
383
|
-
const result = await plugin.process(
|
|
384
|
-
{
|
|
385
|
-
path: "/test",
|
|
386
|
-
route: {},
|
|
387
|
-
method: "GET",
|
|
388
|
-
params: {},
|
|
389
|
-
query: { sort: "name", order: "asc" },
|
|
390
|
-
headers: {},
|
|
391
|
-
state: new Map(),
|
|
392
|
-
},
|
|
393
|
-
items,
|
|
394
|
-
);
|
|
395
|
-
|
|
396
|
-
expect(result.response[0].name).toBe("Alice");
|
|
397
|
-
expect(result.response[1].name).toBe("Bob");
|
|
398
|
-
expect(result.response[2].name).toBeUndefined();
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
it("maintains relative order for items with equal sort values", async () => {
|
|
402
|
-
const plugin = queryPlugin({
|
|
403
|
-
sorting: { allowed: ["priority"], default: "priority" },
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
const items = [
|
|
407
|
-
{ id: 1, priority: 1, label: "first" },
|
|
408
|
-
{ id: 2, priority: 1, label: "second" },
|
|
409
|
-
{ id: 3, priority: 1, label: "third" },
|
|
410
|
-
];
|
|
411
|
-
|
|
412
|
-
const result = await plugin.process(
|
|
413
|
-
{
|
|
414
|
-
path: "/test",
|
|
415
|
-
route: {},
|
|
416
|
-
method: "GET",
|
|
417
|
-
params: {},
|
|
418
|
-
query: { sort: "priority", order: "asc" },
|
|
419
|
-
headers: {},
|
|
420
|
-
state: new Map(),
|
|
421
|
-
},
|
|
422
|
-
items,
|
|
423
|
-
);
|
|
424
|
-
|
|
425
|
-
// Sort stability: equal values preserve insertion order
|
|
426
|
-
expect(result.response.map((i: any) => i.label)).toEqual([
|
|
427
|
-
"first",
|
|
428
|
-
"second",
|
|
429
|
-
"third",
|
|
430
|
-
]);
|
|
431
|
-
});
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
describe("pagination edge cases", () => {
|
|
435
|
-
const makePaginationPlugin = (defaultLimit = 10, maxLimit = 100) =>
|
|
436
|
-
queryPlugin({
|
|
437
|
-
pagination: { defaultLimit, maxLimit },
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
const items = Array.from({ length: 5 }, (_, i) => ({ id: i + 1 }));
|
|
441
|
-
|
|
442
|
-
const makeContext = (query: Record<string, string>) => ({
|
|
443
|
-
path: "/test",
|
|
444
|
-
route: {},
|
|
445
|
-
method: "GET" as const,
|
|
446
|
-
params: {},
|
|
447
|
-
query,
|
|
448
|
-
headers: {},
|
|
449
|
-
state: new Map(),
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
it("clamps page=0 to page 1", async () => {
|
|
453
|
-
const plugin = makePaginationPlugin(2);
|
|
454
|
-
const result = await plugin.process(makeContext({ page: "0" }), items);
|
|
455
|
-
|
|
456
|
-
expect(result.response.pagination.page).toBe(1);
|
|
457
|
-
expect(result.response.data).toEqual([{ id: 1 }, { id: 2 }]);
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
it("clamps page=-1 to page 1", async () => {
|
|
461
|
-
const plugin = makePaginationPlugin(2);
|
|
462
|
-
const result = await plugin.process(makeContext({ page: "-1" }), items);
|
|
463
|
-
|
|
464
|
-
expect(result.response.pagination.page).toBe(1);
|
|
465
|
-
expect(result.response.data).toEqual([{ id: 1 }, { id: 2 }]);
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
it("clamps limit=0 to defaultLimit (0 is falsy, falls back to default)", async () => {
|
|
469
|
-
const plugin = makePaginationPlugin(10, 100);
|
|
470
|
-
const result = await plugin.process(makeContext({ limit: "0" }), items);
|
|
471
|
-
|
|
472
|
-
// parseInt("0") is 0, which is falsy → falls back to defaultLimit (10)
|
|
473
|
-
expect(result.response.pagination.limit).toBe(10);
|
|
474
|
-
expect(result.response.data).toHaveLength(5);
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
it("clamps limit exceeding maxLimit to maxLimit", async () => {
|
|
478
|
-
const plugin = makePaginationPlugin(10, 3);
|
|
479
|
-
const result = await plugin.process(makeContext({ limit: "999" }), items);
|
|
480
|
-
|
|
481
|
-
expect(result.response.pagination.limit).toBe(3);
|
|
482
|
-
expect(result.response.data).toHaveLength(3);
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
it("defaults gracefully when page is non-numeric", async () => {
|
|
486
|
-
const plugin = makePaginationPlugin(2);
|
|
487
|
-
const result = await plugin.process(makeContext({ page: "abc" }), items);
|
|
488
|
-
|
|
489
|
-
expect(result.response.pagination.page).toBe(1);
|
|
490
|
-
expect(result.response.data).toEqual([{ id: 1 }, { id: 2 }]);
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
it("defaults gracefully when limit is non-numeric", async () => {
|
|
494
|
-
const plugin = makePaginationPlugin(2);
|
|
495
|
-
const result = await plugin.process(makeContext({ limit: "abc" }), items);
|
|
496
|
-
|
|
497
|
-
expect(result.response.pagination.limit).toBe(2);
|
|
498
|
-
expect(result.response.data).toEqual([{ id: 1 }, { id: 2 }]);
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
it("returns empty data with correct total for a very large page number", async () => {
|
|
502
|
-
const plugin = makePaginationPlugin(2);
|
|
503
|
-
const result = await plugin.process(makeContext({ page: "9999" }), items);
|
|
504
|
-
|
|
505
|
-
expect(result.response.data).toEqual([]);
|
|
506
|
-
expect(result.response.pagination.total).toBe(5);
|
|
507
|
-
expect(result.response.pagination.totalPages).toBe(3);
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
it("returns data=[], total=0, totalPages=0 for empty array input", async () => {
|
|
511
|
-
const plugin = makePaginationPlugin(10);
|
|
512
|
-
const result = await plugin.process(makeContext({}), []);
|
|
513
|
-
|
|
514
|
-
expect(result.response.data).toEqual([]);
|
|
515
|
-
expect(result.response.pagination.total).toBe(0);
|
|
516
|
-
expect(result.response.pagination.totalPages).toBe(0);
|
|
517
|
-
});
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
describe("process method edge cases", () => {
|
|
521
|
-
it("passes through null response unchanged", async () => {
|
|
522
|
-
const plugin = queryPlugin({
|
|
523
|
-
pagination: { defaultLimit: 10 },
|
|
524
|
-
filtering: { allowed: ["role"] },
|
|
525
|
-
sorting: { allowed: ["name"] },
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
const result = await plugin.process(
|
|
529
|
-
{
|
|
530
|
-
path: "/test",
|
|
531
|
-
route: {},
|
|
532
|
-
method: "GET",
|
|
533
|
-
params: {},
|
|
534
|
-
query: {},
|
|
535
|
-
headers: {},
|
|
536
|
-
state: new Map(),
|
|
537
|
-
},
|
|
538
|
-
null,
|
|
539
|
-
);
|
|
540
|
-
|
|
541
|
-
expect(result.response).toBeNull();
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
it("passes through object (non-array) response unchanged", async () => {
|
|
545
|
-
const plugin = queryPlugin({
|
|
546
|
-
pagination: { defaultLimit: 10 },
|
|
547
|
-
filtering: { allowed: ["role"] },
|
|
548
|
-
sorting: { allowed: ["name"] },
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
const obj = { message: "hello", count: 42 };
|
|
552
|
-
const result = await plugin.process(
|
|
553
|
-
{
|
|
554
|
-
path: "/test",
|
|
555
|
-
route: {},
|
|
556
|
-
method: "GET",
|
|
557
|
-
params: {},
|
|
558
|
-
query: {},
|
|
559
|
-
headers: {},
|
|
560
|
-
state: new Map(),
|
|
561
|
-
},
|
|
562
|
-
obj,
|
|
563
|
-
);
|
|
564
|
-
|
|
565
|
-
expect(result.response).toEqual(obj);
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
it("passes through undefined response unchanged", async () => {
|
|
569
|
-
const plugin = queryPlugin({
|
|
570
|
-
pagination: { defaultLimit: 10 },
|
|
571
|
-
filtering: { allowed: ["role"] },
|
|
572
|
-
sorting: { allowed: ["name"] },
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
const result = await plugin.process(
|
|
576
|
-
{
|
|
577
|
-
path: "/test",
|
|
578
|
-
route: {},
|
|
579
|
-
method: "GET",
|
|
580
|
-
params: {},
|
|
581
|
-
query: {},
|
|
582
|
-
headers: {},
|
|
583
|
-
state: new Map(),
|
|
584
|
-
},
|
|
585
|
-
undefined,
|
|
586
|
-
);
|
|
587
|
-
|
|
588
|
-
expect(result.response).toBeUndefined();
|
|
589
|
-
});
|
|
590
|
-
});
|
|
591
|
-
});
|
package/src/index.ts
DELETED
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
/// <reference path="../../core/schmock.d.ts" />
|
|
2
|
-
|
|
3
|
-
import { isStatusTuple } from "@schmock/core";
|
|
4
|
-
import { version as packageVersion } from "../package.json";
|
|
5
|
-
|
|
6
|
-
interface PaginationOptions {
|
|
7
|
-
/** Default items per page (default: 10) */
|
|
8
|
-
defaultLimit?: number;
|
|
9
|
-
/** Maximum items per page (default: 100) */
|
|
10
|
-
maxLimit?: number;
|
|
11
|
-
/** Query parameter name for page number (default: "page") */
|
|
12
|
-
pageParam?: string;
|
|
13
|
-
/** Query parameter name for limit (default: "limit") */
|
|
14
|
-
limitParam?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface SortingOptions {
|
|
18
|
-
/** Fields allowed for sorting */
|
|
19
|
-
allowed: string[];
|
|
20
|
-
/** Default sort field */
|
|
21
|
-
default?: string;
|
|
22
|
-
/** Default sort order (default: "asc") */
|
|
23
|
-
defaultOrder?: "asc" | "desc";
|
|
24
|
-
/** Query parameter name for sort field (default: "sort") */
|
|
25
|
-
sortParam?: string;
|
|
26
|
-
/** Query parameter name for sort order (default: "order") */
|
|
27
|
-
orderParam?: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface FilteringOptions {
|
|
31
|
-
/** Fields allowed for filtering */
|
|
32
|
-
allowed: string[];
|
|
33
|
-
/** Query parameter prefix for filters (default: "filter") */
|
|
34
|
-
filterPrefix?: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface QueryPluginOptions {
|
|
38
|
-
pagination?: PaginationOptions;
|
|
39
|
-
sorting?: SortingOptions;
|
|
40
|
-
filtering?: FilteringOptions;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
44
|
-
return typeof value === "object" && value !== null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function isStringRecord(value: unknown): value is Record<string, string> {
|
|
48
|
-
return (
|
|
49
|
-
isRecord(value) &&
|
|
50
|
-
Object.values(value).every((entry) => typeof entry === "string")
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function isStructuredResponse(value: unknown): value is {
|
|
55
|
-
status: number;
|
|
56
|
-
body: unknown;
|
|
57
|
-
headers?: Record<string, string>;
|
|
58
|
-
} {
|
|
59
|
-
return (
|
|
60
|
-
isRecord(value) &&
|
|
61
|
-
!Array.isArray(value) &&
|
|
62
|
-
typeof value.status === "number" &&
|
|
63
|
-
"body" in value &&
|
|
64
|
-
(value.headers === undefined || isStringRecord(value.headers))
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function getResponseBody(response: unknown): unknown {
|
|
69
|
-
if (isStatusTuple(response)) return response[1];
|
|
70
|
-
if (isStructuredResponse(response)) return response.body;
|
|
71
|
-
return response;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function replaceResponseBody(response: unknown, body: unknown): unknown {
|
|
75
|
-
if (isStatusTuple(response)) {
|
|
76
|
-
return response.length === 3
|
|
77
|
-
? [response[0], body, response[2]]
|
|
78
|
-
: [response[0], body];
|
|
79
|
-
}
|
|
80
|
-
if (isStructuredResponse(response)) return { ...response, body };
|
|
81
|
-
return body;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function queryPlugin(options: QueryPluginOptions): Schmock.Plugin {
|
|
85
|
-
return {
|
|
86
|
-
name: "query",
|
|
87
|
-
version: packageVersion,
|
|
88
|
-
|
|
89
|
-
process(
|
|
90
|
-
context: Schmock.PluginContext,
|
|
91
|
-
response?: unknown,
|
|
92
|
-
): Schmock.PluginResult {
|
|
93
|
-
const responseBody = getResponseBody(response);
|
|
94
|
-
if (!Array.isArray(responseBody)) {
|
|
95
|
-
return { context, response };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
let items: unknown[] = [...responseBody];
|
|
99
|
-
const query = context.query || {};
|
|
100
|
-
|
|
101
|
-
// Apply filtering
|
|
102
|
-
if (options.filtering) {
|
|
103
|
-
items = applyFiltering(items, query, options.filtering);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Apply sorting
|
|
107
|
-
if (options.sorting) {
|
|
108
|
-
items = applySorting(items, query, options.sorting);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Apply pagination
|
|
112
|
-
if (options.pagination) {
|
|
113
|
-
const result = applyPagination(items, query, options.pagination);
|
|
114
|
-
return { context, response: replaceResponseBody(response, result) };
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return { context, response: replaceResponseBody(response, items) };
|
|
118
|
-
},
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function applyFiltering(
|
|
123
|
-
items: unknown[],
|
|
124
|
-
query: Record<string, string>,
|
|
125
|
-
options: FilteringOptions,
|
|
126
|
-
): unknown[] {
|
|
127
|
-
const prefix = options.filterPrefix ?? "filter";
|
|
128
|
-
let result = items;
|
|
129
|
-
|
|
130
|
-
for (const field of options.allowed) {
|
|
131
|
-
// Support filter[field]=value format
|
|
132
|
-
const bracketKey = `${prefix}[${field}]`;
|
|
133
|
-
// Support filter.field=value format
|
|
134
|
-
const dotKey = `${prefix}.${field}`;
|
|
135
|
-
// Support plain field=value format as fallback
|
|
136
|
-
const value = query[bracketKey] ?? query[dotKey] ?? query[field];
|
|
137
|
-
|
|
138
|
-
if (value !== undefined) {
|
|
139
|
-
result = result.filter((item) => {
|
|
140
|
-
if (!isRecord(item)) return false;
|
|
141
|
-
const itemValue = item[field];
|
|
142
|
-
if (itemValue === undefined) return false;
|
|
143
|
-
// Intentional string coercion: query params are inherently strings
|
|
144
|
-
return String(itemValue) === value;
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return result;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function applySorting(
|
|
153
|
-
items: unknown[],
|
|
154
|
-
query: Record<string, string>,
|
|
155
|
-
options: SortingOptions,
|
|
156
|
-
): unknown[] {
|
|
157
|
-
const sortParam = options.sortParam ?? "sort";
|
|
158
|
-
const orderParam = options.orderParam ?? "order";
|
|
159
|
-
const sortField = query[sortParam] ?? options.default;
|
|
160
|
-
const rawOrder = query[orderParam] ?? options.defaultOrder ?? "asc";
|
|
161
|
-
const sortOrder = rawOrder === "desc" ? "desc" : "asc";
|
|
162
|
-
|
|
163
|
-
if (!sortField) return items;
|
|
164
|
-
|
|
165
|
-
// Only sort by allowed fields
|
|
166
|
-
if (!options.allowed.includes(sortField)) return items;
|
|
167
|
-
|
|
168
|
-
return items.sort((a, b) => {
|
|
169
|
-
if (!isRecord(a) || !isRecord(b)) return 0;
|
|
170
|
-
const aVal = a[sortField];
|
|
171
|
-
const bVal = b[sortField];
|
|
172
|
-
|
|
173
|
-
if (aVal === bVal) return 0;
|
|
174
|
-
if (aVal === undefined) return 1;
|
|
175
|
-
if (bVal === undefined) return -1;
|
|
176
|
-
|
|
177
|
-
let comparison: number;
|
|
178
|
-
if (typeof aVal === "number" && typeof bVal === "number") {
|
|
179
|
-
comparison = aVal - bVal;
|
|
180
|
-
} else {
|
|
181
|
-
comparison = String(aVal).localeCompare(String(bVal));
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return sortOrder === "desc" ? -comparison : comparison;
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
interface PaginatedResult {
|
|
189
|
-
data: unknown[];
|
|
190
|
-
pagination: {
|
|
191
|
-
page: number;
|
|
192
|
-
limit: number;
|
|
193
|
-
total: number;
|
|
194
|
-
totalPages: number;
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function applyPagination(
|
|
199
|
-
items: unknown[],
|
|
200
|
-
query: Record<string, string>,
|
|
201
|
-
options: PaginationOptions,
|
|
202
|
-
): PaginatedResult {
|
|
203
|
-
const pageParam = options.pageParam ?? "page";
|
|
204
|
-
const limitParam = options.limitParam ?? "limit";
|
|
205
|
-
const defaultLimit = options.defaultLimit ?? 10;
|
|
206
|
-
const maxLimit = options.maxLimit ?? 100;
|
|
207
|
-
|
|
208
|
-
const page = Math.max(1, Number.parseInt(query[pageParam] || "1", 10) || 1);
|
|
209
|
-
const limit = Math.min(
|
|
210
|
-
maxLimit,
|
|
211
|
-
Math.max(
|
|
212
|
-
1,
|
|
213
|
-
Number.parseInt(query[limitParam] || String(defaultLimit), 10) ||
|
|
214
|
-
defaultLimit,
|
|
215
|
-
),
|
|
216
|
-
);
|
|
217
|
-
|
|
218
|
-
const total = items.length;
|
|
219
|
-
const totalPages = Math.ceil(total / limit);
|
|
220
|
-
const start = (page - 1) * limit;
|
|
221
|
-
const data = items.slice(start, start + limit);
|
|
222
|
-
|
|
223
|
-
return {
|
|
224
|
-
data,
|
|
225
|
-
pagination: {
|
|
226
|
-
page,
|
|
227
|
-
limit,
|
|
228
|
-
total,
|
|
229
|
-
totalPages,
|
|
230
|
-
},
|
|
231
|
-
};
|
|
232
|
-
}
|
|
@@ -1,362 +0,0 @@
|
|
|
1
|
-
import { describeFeature, loadFeature } from "@amiceli/vitest-cucumber";
|
|
2
|
-
import { schmock } from "@schmock/core";
|
|
3
|
-
import { expect } from "vitest";
|
|
4
|
-
import { queryPlugin } from "../index";
|
|
5
|
-
|
|
6
|
-
const feature = await loadFeature("../../features/query-plugin.feature");
|
|
7
|
-
|
|
8
|
-
function isUnknownArray(value: unknown): value is unknown[] {
|
|
9
|
-
return Array.isArray(value);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
13
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function requireRecord(
|
|
17
|
-
value: unknown,
|
|
18
|
-
description: string,
|
|
19
|
-
): Record<string, unknown> {
|
|
20
|
-
if (!isRecord(value)) {
|
|
21
|
-
throw new Error(`Expected ${description} to be an object`);
|
|
22
|
-
}
|
|
23
|
-
return value;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function requireRecordArray(
|
|
27
|
-
value: unknown,
|
|
28
|
-
description: string,
|
|
29
|
-
): Record<string, unknown>[] {
|
|
30
|
-
if (!isUnknownArray(value)) {
|
|
31
|
-
throw new Error(`Expected ${description} to be an array`);
|
|
32
|
-
}
|
|
33
|
-
return value.map((item) => requireRecord(item, `${description} item`));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function requirePaginatedBody(response: Schmock.Response) {
|
|
37
|
-
const body = requireRecord(response.body, "paginated response body");
|
|
38
|
-
return {
|
|
39
|
-
data: requireRecordArray(body.data, "paginated response data"),
|
|
40
|
-
pagination: requireRecord(body.pagination, "pagination metadata"),
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
describeFeature(feature, ({ Scenario, ScenarioOutline }) => {
|
|
45
|
-
let mock: Schmock.CallableMockInstance;
|
|
46
|
-
let response: Schmock.Response;
|
|
47
|
-
|
|
48
|
-
// Generate test data
|
|
49
|
-
const generateItems = (count: number) =>
|
|
50
|
-
Array.from({ length: count }, (_, i) => ({
|
|
51
|
-
id: i + 1,
|
|
52
|
-
name: `Item ${i + 1}`,
|
|
53
|
-
}));
|
|
54
|
-
|
|
55
|
-
const namedItems = [
|
|
56
|
-
{ id: 1, name: "Charlie" },
|
|
57
|
-
{ id: 2, name: "Alice" },
|
|
58
|
-
{ id: 3, name: "Bob" },
|
|
59
|
-
];
|
|
60
|
-
|
|
61
|
-
const categorizedItems = [
|
|
62
|
-
{ id: 1, name: "Alice", role: "admin" },
|
|
63
|
-
{ id: 2, name: "Bob", role: "user" },
|
|
64
|
-
{ id: 3, name: "Charlie", role: "admin" },
|
|
65
|
-
{ id: 4, name: "Dave", role: "user" },
|
|
66
|
-
{ id: 5, name: "Eve", role: "moderator" },
|
|
67
|
-
];
|
|
68
|
-
|
|
69
|
-
Scenario(
|
|
70
|
-
"Basic pagination with default settings",
|
|
71
|
-
({ Given, When, Then, And }) => {
|
|
72
|
-
Given("I create a mock with 25 items and pagination plugin", () => {
|
|
73
|
-
mock = schmock();
|
|
74
|
-
mock("GET /items", generateItems(25)).pipe(
|
|
75
|
-
queryPlugin({
|
|
76
|
-
pagination: { defaultLimit: 10, maxLimit: 100 },
|
|
77
|
-
}),
|
|
78
|
-
);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
When("I request page 1", async () => {
|
|
82
|
-
response = await mock.handle("GET", "/items", {
|
|
83
|
-
query: { page: "1" },
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
Then("I should receive {int} items", (_, count: number) => {
|
|
88
|
-
expect(requirePaginatedBody(response).data).toHaveLength(count);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
And("the pagination total should be {int}", (_, total: number) => {
|
|
92
|
-
expect(requirePaginatedBody(response).pagination.total).toBe(total);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
And(
|
|
96
|
-
"the pagination totalPages should be {int}",
|
|
97
|
-
(_, totalPages: number) => {
|
|
98
|
-
expect(requirePaginatedBody(response).pagination.totalPages).toBe(
|
|
99
|
-
totalPages,
|
|
100
|
-
);
|
|
101
|
-
},
|
|
102
|
-
);
|
|
103
|
-
},
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
Scenario("Pagination with custom limit", ({ Given, When, Then, And }) => {
|
|
107
|
-
Given("I create a mock with 25 items and pagination plugin", () => {
|
|
108
|
-
mock = schmock();
|
|
109
|
-
mock("GET /items", generateItems(25)).pipe(
|
|
110
|
-
queryPlugin({
|
|
111
|
-
pagination: { defaultLimit: 10, maxLimit: 100 },
|
|
112
|
-
}),
|
|
113
|
-
);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
When("I request page 1 with limit 5", async () => {
|
|
117
|
-
response = await mock.handle("GET", "/items", {
|
|
118
|
-
query: { page: "1", limit: "5" },
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
Then("I should receive {int} items", (_, count: number) => {
|
|
123
|
-
expect(requirePaginatedBody(response).data).toHaveLength(count);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
And(
|
|
127
|
-
"the pagination totalPages should be {int}",
|
|
128
|
-
(_, totalPages: number) => {
|
|
129
|
-
expect(requirePaginatedBody(response).pagination.totalPages).toBe(
|
|
130
|
-
totalPages,
|
|
131
|
-
);
|
|
132
|
-
},
|
|
133
|
-
);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
Scenario("Sort items ascending by name", ({ Given, When, Then, And }) => {
|
|
137
|
-
Given("I create a mock with named items and sorting plugin", () => {
|
|
138
|
-
mock = schmock();
|
|
139
|
-
mock("GET /items", namedItems).pipe(
|
|
140
|
-
queryPlugin({
|
|
141
|
-
sorting: { allowed: ["name", "id"], default: "id" },
|
|
142
|
-
}),
|
|
143
|
-
);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
When(
|
|
147
|
-
"I request with sort {string} order {string}",
|
|
148
|
-
async (_, sort: string, order: string) => {
|
|
149
|
-
response = await mock.handle("GET", "/items", {
|
|
150
|
-
query: { sort, order },
|
|
151
|
-
});
|
|
152
|
-
},
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
Then("the first item name should be {string}", (_, name: string) => {
|
|
156
|
-
const items = requireRecordArray(response.body, "sorted response body");
|
|
157
|
-
expect(items.at(0)?.name).toBe(name);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
And("the last item name should be {string}", (_, name: string) => {
|
|
161
|
-
const items = requireRecordArray(response.body, "sorted response body");
|
|
162
|
-
expect(items.at(-1)?.name).toBe(name);
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
Scenario("Sort items descending by name", ({ Given, When, Then, And }) => {
|
|
167
|
-
Given("I create a mock with named items and sorting plugin", () => {
|
|
168
|
-
mock = schmock();
|
|
169
|
-
mock("GET /items", namedItems).pipe(
|
|
170
|
-
queryPlugin({
|
|
171
|
-
sorting: { allowed: ["name", "id"], default: "id" },
|
|
172
|
-
}),
|
|
173
|
-
);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
When(
|
|
177
|
-
"I request with sort {string} order {string}",
|
|
178
|
-
async (_, sort: string, order: string) => {
|
|
179
|
-
response = await mock.handle("GET", "/items", {
|
|
180
|
-
query: { sort, order },
|
|
181
|
-
});
|
|
182
|
-
},
|
|
183
|
-
);
|
|
184
|
-
|
|
185
|
-
Then("the first item name should be {string}", (_, name: string) => {
|
|
186
|
-
const items = requireRecordArray(response.body, "sorted response body");
|
|
187
|
-
expect(items.at(0)?.name).toBe(name);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
And("the last item name should be {string}", (_, name: string) => {
|
|
191
|
-
const items = requireRecordArray(response.body, "sorted response body");
|
|
192
|
-
expect(items.at(-1)?.name).toBe(name);
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
Scenario("Filter items by field value", ({ Given, When, Then, And }) => {
|
|
197
|
-
Given("I create a mock with categorized items and filtering plugin", () => {
|
|
198
|
-
mock = schmock();
|
|
199
|
-
mock("GET /items", categorizedItems).pipe(
|
|
200
|
-
queryPlugin({
|
|
201
|
-
filtering: { allowed: ["role"] },
|
|
202
|
-
}),
|
|
203
|
-
);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
When("I request with filter role {string}", async (_, role: string) => {
|
|
207
|
-
response = await mock.handle("GET", "/items", {
|
|
208
|
-
query: { "filter[role]": role },
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
Then("I should receive {int} filtered items", (_, count: number) => {
|
|
213
|
-
expect(
|
|
214
|
-
requireRecordArray(response.body, "filtered response body"),
|
|
215
|
-
).toHaveLength(count);
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
And("all items should have role {string}", (_, role: string) => {
|
|
219
|
-
const items = requireRecordArray(response.body, "filtered response body");
|
|
220
|
-
for (const item of items) {
|
|
221
|
-
expect(item.role).toBe(role);
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
Scenario(
|
|
227
|
-
"Combined pagination, sorting, and filtering",
|
|
228
|
-
({ Given, When, Then, And }) => {
|
|
229
|
-
const users = [
|
|
230
|
-
{ id: 1, name: "Dave", role: "admin" },
|
|
231
|
-
{ id: 2, name: "Alice", role: "user" },
|
|
232
|
-
{ id: 3, name: "Charlie", role: "admin" },
|
|
233
|
-
{ id: 4, name: "Bob", role: "admin" },
|
|
234
|
-
{ id: 5, name: "Eve", role: "user" },
|
|
235
|
-
{ id: 6, name: "Frank", role: "admin" },
|
|
236
|
-
{ id: 7, name: "Grace", role: "user" },
|
|
237
|
-
{ id: 8, name: "Hank", role: "admin" },
|
|
238
|
-
{ id: 9, name: "Ivy", role: "user" },
|
|
239
|
-
{ id: 10, name: "Jack", role: "admin" },
|
|
240
|
-
{ id: 11, name: "Karen", role: "user" },
|
|
241
|
-
{ id: 12, name: "Leo", role: "admin" },
|
|
242
|
-
{ id: 13, name: "Mona", role: "user" },
|
|
243
|
-
{ id: 14, name: "Nate", role: "admin" },
|
|
244
|
-
{ id: 15, name: "Olivia", role: "user" },
|
|
245
|
-
{ id: 16, name: "Paul", role: "admin" },
|
|
246
|
-
{ id: 17, name: "Quinn", role: "user" },
|
|
247
|
-
{ id: 18, name: "Rose", role: "admin" },
|
|
248
|
-
{ id: 19, name: "Sam", role: "user" },
|
|
249
|
-
{ id: 20, name: "Tina", role: "admin" },
|
|
250
|
-
];
|
|
251
|
-
|
|
252
|
-
Given("I create a mock with 20 users and full query plugin", () => {
|
|
253
|
-
mock = schmock();
|
|
254
|
-
mock("GET /users", users).pipe(
|
|
255
|
-
queryPlugin({
|
|
256
|
-
pagination: { defaultLimit: 10, maxLimit: 100 },
|
|
257
|
-
sorting: { allowed: ["name", "id"] },
|
|
258
|
-
filtering: { allowed: ["role"] },
|
|
259
|
-
}),
|
|
260
|
-
);
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
When(
|
|
264
|
-
"I request page 1 with limit 2 filter role {string} and sort {string}",
|
|
265
|
-
async (_, role: string, sort: string) => {
|
|
266
|
-
response = await mock.handle("GET", "/users", {
|
|
267
|
-
query: { page: "1", limit: "2", "filter[role]": role, sort },
|
|
268
|
-
});
|
|
269
|
-
},
|
|
270
|
-
);
|
|
271
|
-
|
|
272
|
-
Then("I should receive {int} items", (_, count: number) => {
|
|
273
|
-
expect(requirePaginatedBody(response).data).toHaveLength(count);
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
And("the items should be sorted by name ascending", () => {
|
|
277
|
-
const names = requirePaginatedBody(response).data.map((item) => {
|
|
278
|
-
if (typeof item.name !== "string") {
|
|
279
|
-
throw new Error(
|
|
280
|
-
"Expected each paginated item to have a string name",
|
|
281
|
-
);
|
|
282
|
-
}
|
|
283
|
-
return item.name;
|
|
284
|
-
});
|
|
285
|
-
const sorted = [...names].sort();
|
|
286
|
-
expect(names).toEqual(sorted);
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
And("all items should have role {string}", (_, role: string) => {
|
|
290
|
-
for (const item of requirePaginatedBody(response).data) {
|
|
291
|
-
expect(item.role).toBe(role);
|
|
292
|
-
}
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
And("the pagination total should reflect filtered count", () => {
|
|
296
|
-
// 11 admins in the dataset
|
|
297
|
-
expect(requirePaginatedBody(response).pagination.total).toBe(11);
|
|
298
|
-
});
|
|
299
|
-
},
|
|
300
|
-
);
|
|
301
|
-
|
|
302
|
-
ScenarioOutline(
|
|
303
|
-
"Paginate array response containers without losing metadata",
|
|
304
|
-
({ Given, When, Then, And }, variables) => {
|
|
305
|
-
Given("I create a mock returning 5 items in a {string} response", () => {
|
|
306
|
-
mock = schmock();
|
|
307
|
-
const headers = { "x-query-source": variables.container };
|
|
308
|
-
if (variables.container === "tuple") {
|
|
309
|
-
mock(
|
|
310
|
-
"GET /items",
|
|
311
|
-
() =>
|
|
312
|
-
[206, generateItems(5), headers] satisfies [
|
|
313
|
-
number,
|
|
314
|
-
unknown,
|
|
315
|
-
Record<string, string>,
|
|
316
|
-
],
|
|
317
|
-
);
|
|
318
|
-
} else if (variables.container === "structured") {
|
|
319
|
-
mock("GET /items", () => ({
|
|
320
|
-
status: 206,
|
|
321
|
-
body: generateItems(5),
|
|
322
|
-
headers,
|
|
323
|
-
}));
|
|
324
|
-
} else {
|
|
325
|
-
throw new Error(
|
|
326
|
-
`Unsupported query response container: ${variables.container}`,
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
mock.pipe(
|
|
330
|
-
queryPlugin({
|
|
331
|
-
pagination: { defaultLimit: 10, maxLimit: 100 },
|
|
332
|
-
}),
|
|
333
|
-
);
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
When("I request page 2 with limit 2", async () => {
|
|
337
|
-
response = await mock.handle("GET", "/items", {
|
|
338
|
-
query: { page: "2", limit: "2" },
|
|
339
|
-
});
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
Then("the response status should be 206", () => {
|
|
343
|
-
expect(response.status).toBe(206);
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
And(
|
|
347
|
-
"response header {string} should be {string}",
|
|
348
|
-
(_, header: string) => {
|
|
349
|
-
expect(response.headers[header]).toBe(variables.container);
|
|
350
|
-
},
|
|
351
|
-
);
|
|
352
|
-
|
|
353
|
-
And("I should receive 2 items", () => {
|
|
354
|
-
expect(requirePaginatedBody(response).data).toHaveLength(2);
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
And("the pagination total should be 5", () => {
|
|
358
|
-
expect(requirePaginatedBody(response).pagination.total).toBe(5);
|
|
359
|
-
});
|
|
360
|
-
},
|
|
361
|
-
);
|
|
362
|
-
});
|