@schmock/query 1.1.0

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.
@@ -0,0 +1,36 @@
1
+ interface PaginationOptions {
2
+ /** Default items per page (default: 10) */
3
+ defaultLimit?: number;
4
+ /** Maximum items per page (default: 100) */
5
+ maxLimit?: number;
6
+ /** Query parameter name for page number (default: "page") */
7
+ pageParam?: string;
8
+ /** Query parameter name for limit (default: "limit") */
9
+ limitParam?: string;
10
+ }
11
+ interface SortingOptions {
12
+ /** Fields allowed for sorting */
13
+ allowed: string[];
14
+ /** Default sort field */
15
+ default?: string;
16
+ /** Default sort order (default: "asc") */
17
+ defaultOrder?: "asc" | "desc";
18
+ /** Query parameter name for sort field (default: "sort") */
19
+ sortParam?: string;
20
+ /** Query parameter name for sort order (default: "order") */
21
+ orderParam?: string;
22
+ }
23
+ interface FilteringOptions {
24
+ /** Fields allowed for filtering */
25
+ allowed: string[];
26
+ /** Query parameter prefix for filters (default: "filter") */
27
+ filterPrefix?: string;
28
+ }
29
+ interface QueryPluginOptions {
30
+ pagination?: PaginationOptions;
31
+ sorting?: SortingOptions;
32
+ filtering?: FilteringOptions;
33
+ }
34
+ export declare function queryPlugin(options: QueryPluginOptions): Schmock.Plugin;
35
+ export {};
36
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,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 ADDED
@@ -0,0 +1,102 @@
1
+ /// <reference path="../../../types/schmock.d.ts" />
2
+ export function queryPlugin(options) {
3
+ return {
4
+ name: "query",
5
+ version: "1.0.0",
6
+ process(context, response) {
7
+ // Only process array responses
8
+ if (!Array.isArray(response)) {
9
+ return { context, response };
10
+ }
11
+ let items = [...response];
12
+ const query = context.query || {};
13
+ // Apply filtering
14
+ if (options.filtering) {
15
+ items = applyFiltering(items, query, options.filtering);
16
+ }
17
+ // Apply sorting
18
+ if (options.sorting) {
19
+ items = applySorting(items, query, options.sorting);
20
+ }
21
+ // Apply pagination
22
+ if (options.pagination) {
23
+ const result = applyPagination(items, query, options.pagination);
24
+ return { context, response: result };
25
+ }
26
+ return { context, response: items };
27
+ },
28
+ };
29
+ }
30
+ function applyFiltering(items, query, options) {
31
+ const prefix = options.filterPrefix ?? "filter";
32
+ let result = items;
33
+ for (const field of options.allowed) {
34
+ // Support filter[field]=value format
35
+ const bracketKey = `${prefix}[${field}]`;
36
+ // Support filter.field=value format
37
+ const dotKey = `${prefix}.${field}`;
38
+ // Support plain field=value format as fallback
39
+ const value = query[bracketKey] ?? query[dotKey] ?? query[field];
40
+ if (value !== undefined) {
41
+ result = result.filter((item) => {
42
+ const itemValue = item[field];
43
+ if (itemValue === undefined)
44
+ return false;
45
+ return String(itemValue) === value;
46
+ });
47
+ }
48
+ }
49
+ return result;
50
+ }
51
+ function applySorting(items, query, options) {
52
+ const sortParam = options.sortParam ?? "sort";
53
+ const orderParam = options.orderParam ?? "order";
54
+ const sortField = query[sortParam] ?? options.default;
55
+ const rawOrder = query[orderParam] ?? options.defaultOrder ?? "asc";
56
+ const sortOrder = rawOrder === "desc" ? "desc" : "asc";
57
+ if (!sortField)
58
+ return items;
59
+ // Only sort by allowed fields
60
+ if (!options.allowed.includes(sortField))
61
+ return items;
62
+ return items.sort((a, b) => {
63
+ const aVal = a[sortField];
64
+ const bVal = b[sortField];
65
+ if (aVal === bVal)
66
+ return 0;
67
+ if (aVal === undefined)
68
+ return 1;
69
+ if (bVal === undefined)
70
+ return -1;
71
+ let comparison;
72
+ if (typeof aVal === "number" && typeof bVal === "number") {
73
+ comparison = aVal - bVal;
74
+ }
75
+ else {
76
+ comparison = String(aVal).localeCompare(String(bVal));
77
+ }
78
+ return sortOrder === "desc" ? -comparison : comparison;
79
+ });
80
+ }
81
+ function applyPagination(items, query, options) {
82
+ const pageParam = options.pageParam ?? "page";
83
+ const limitParam = options.limitParam ?? "limit";
84
+ const defaultLimit = options.defaultLimit ?? 10;
85
+ const maxLimit = options.maxLimit ?? 100;
86
+ const page = Math.max(1, Number.parseInt(query[pageParam] || "1", 10) || 1);
87
+ const limit = Math.min(maxLimit, Math.max(1, Number.parseInt(query[limitParam] || String(defaultLimit), 10) ||
88
+ defaultLimit));
89
+ const total = items.length;
90
+ const totalPages = Math.ceil(total / limit);
91
+ const start = (page - 1) * limit;
92
+ const data = items.slice(start, start + limit);
93
+ return {
94
+ data,
95
+ pagination: {
96
+ page,
97
+ limit,
98
+ total,
99
+ totalPages,
100
+ },
101
+ };
102
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@schmock/query",
3
+ "description": "Pagination, filtering, and sorting plugin for Schmock",
4
+ "version": "1.1.0",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "src"
11
+ ],
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js",
16
+ "default": "./dist/index.js"
17
+ },
18
+ "./package.json": "./package.json"
19
+ },
20
+ "scripts": {
21
+ "build": "bun build:lib && bun build:types",
22
+ "build:lib": "bun build --minify --outdir=dist src/index.ts",
23
+ "build:types": "tsc -p tsconfig.json",
24
+ "test": "vitest",
25
+ "test:watch": "vitest --watch",
26
+ "test:bdd": "vitest run --config vitest.config.bdd.ts",
27
+ "lint": "biome check src/*.ts",
28
+ "lint:fix": "biome check --write --unsafe src/*.ts"
29
+ },
30
+ "license": "MIT",
31
+ "peerDependencies": {
32
+ "@schmock/core": "^1.0.0"
33
+ },
34
+ "devDependencies": {
35
+ "@amiceli/vitest-cucumber": "^6.2.0",
36
+ "@types/node": "^25.1.0",
37
+ "vitest": "^4.0.15"
38
+ }
39
+ }
@@ -0,0 +1,134 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { queryPlugin } from "./index";
3
+
4
+ describe("queryPlugin", () => {
5
+ it("creates a plugin with correct name", () => {
6
+ const plugin = queryPlugin({ pagination: {} });
7
+ expect(plugin.name).toBe("query");
8
+ expect(plugin.version).toBe("1.0.0");
9
+ });
10
+
11
+ it("passes through non-array responses", async () => {
12
+ const plugin = queryPlugin({ pagination: {} });
13
+ const result = await plugin.process(
14
+ {
15
+ path: "/test",
16
+ route: {},
17
+ method: "GET",
18
+ params: {},
19
+ query: {},
20
+ headers: {},
21
+ state: new Map(),
22
+ },
23
+ { message: "not an array" },
24
+ );
25
+
26
+ expect(result.response).toEqual({ message: "not an array" });
27
+ });
28
+
29
+ it("paginates array responses", async () => {
30
+ const plugin = queryPlugin({
31
+ pagination: { defaultLimit: 2, maxLimit: 10 },
32
+ });
33
+
34
+ const items = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }];
35
+ const result = await plugin.process(
36
+ {
37
+ path: "/test",
38
+ route: {},
39
+ method: "GET",
40
+ params: {},
41
+ query: { page: "2", limit: "2" },
42
+ headers: {},
43
+ state: new Map(),
44
+ },
45
+ items,
46
+ );
47
+
48
+ expect(result.response).toEqual({
49
+ data: [{ id: 3 }, { id: 4 }],
50
+ pagination: { page: 2, limit: 2, total: 5, totalPages: 3 },
51
+ });
52
+ });
53
+
54
+ it("sorts array responses", async () => {
55
+ const plugin = queryPlugin({
56
+ sorting: { allowed: ["name"], default: "name" },
57
+ });
58
+
59
+ const items = [
60
+ { id: 3, name: "Charlie" },
61
+ { id: 1, name: "Alice" },
62
+ { id: 2, name: "Bob" },
63
+ ];
64
+
65
+ const result = await plugin.process(
66
+ {
67
+ path: "/test",
68
+ route: {},
69
+ method: "GET",
70
+ params: {},
71
+ query: { sort: "name", order: "asc" },
72
+ headers: {},
73
+ state: new Map(),
74
+ },
75
+ items,
76
+ );
77
+
78
+ expect(result.response.map((i: any) => i.name)).toEqual([
79
+ "Alice",
80
+ "Bob",
81
+ "Charlie",
82
+ ]);
83
+ });
84
+
85
+ it("filters array responses", async () => {
86
+ const plugin = queryPlugin({
87
+ filtering: { allowed: ["role"] },
88
+ });
89
+
90
+ const items = [
91
+ { id: 1, name: "Alice", role: "admin" },
92
+ { id: 2, name: "Bob", role: "user" },
93
+ { id: 3, name: "Charlie", role: "admin" },
94
+ ];
95
+
96
+ const result = await plugin.process(
97
+ {
98
+ path: "/test",
99
+ route: {},
100
+ method: "GET",
101
+ params: {},
102
+ query: { "filter[role]": "admin" },
103
+ headers: {},
104
+ state: new Map(),
105
+ },
106
+ items,
107
+ );
108
+
109
+ expect(result.response).toHaveLength(2);
110
+ expect(result.response.every((i: any) => i.role === "admin")).toBe(true);
111
+ });
112
+
113
+ it("respects maxLimit", async () => {
114
+ const plugin = queryPlugin({
115
+ pagination: { defaultLimit: 5, maxLimit: 3 },
116
+ });
117
+
118
+ const items = Array.from({ length: 10 }, (_, i) => ({ id: i }));
119
+ const result = await plugin.process(
120
+ {
121
+ path: "/test",
122
+ route: {},
123
+ method: "GET",
124
+ params: {},
125
+ query: { limit: "100" },
126
+ headers: {},
127
+ state: new Map(),
128
+ },
129
+ items,
130
+ );
131
+
132
+ expect(result.response.data).toHaveLength(3);
133
+ });
134
+ });
package/src/index.ts ADDED
@@ -0,0 +1,185 @@
1
+ /// <reference path="../../../types/schmock.d.ts" />
2
+
3
+ interface PaginationOptions {
4
+ /** Default items per page (default: 10) */
5
+ defaultLimit?: number;
6
+ /** Maximum items per page (default: 100) */
7
+ maxLimit?: number;
8
+ /** Query parameter name for page number (default: "page") */
9
+ pageParam?: string;
10
+ /** Query parameter name for limit (default: "limit") */
11
+ limitParam?: string;
12
+ }
13
+
14
+ interface SortingOptions {
15
+ /** Fields allowed for sorting */
16
+ allowed: string[];
17
+ /** Default sort field */
18
+ default?: string;
19
+ /** Default sort order (default: "asc") */
20
+ defaultOrder?: "asc" | "desc";
21
+ /** Query parameter name for sort field (default: "sort") */
22
+ sortParam?: string;
23
+ /** Query parameter name for sort order (default: "order") */
24
+ orderParam?: string;
25
+ }
26
+
27
+ interface FilteringOptions {
28
+ /** Fields allowed for filtering */
29
+ allowed: string[];
30
+ /** Query parameter prefix for filters (default: "filter") */
31
+ filterPrefix?: string;
32
+ }
33
+
34
+ interface QueryPluginOptions {
35
+ pagination?: PaginationOptions;
36
+ sorting?: SortingOptions;
37
+ filtering?: FilteringOptions;
38
+ }
39
+
40
+ export function queryPlugin(options: QueryPluginOptions): Schmock.Plugin {
41
+ return {
42
+ name: "query",
43
+ version: "1.0.0",
44
+
45
+ process(
46
+ context: Schmock.PluginContext,
47
+ response?: any,
48
+ ): Schmock.PluginResult {
49
+ // Only process array responses
50
+ if (!Array.isArray(response)) {
51
+ return { context, response };
52
+ }
53
+
54
+ let items = [...response];
55
+ const query = context.query || {};
56
+
57
+ // Apply filtering
58
+ if (options.filtering) {
59
+ items = applyFiltering(items, query, options.filtering);
60
+ }
61
+
62
+ // Apply sorting
63
+ if (options.sorting) {
64
+ items = applySorting(items, query, options.sorting);
65
+ }
66
+
67
+ // Apply pagination
68
+ if (options.pagination) {
69
+ const result = applyPagination(items, query, options.pagination);
70
+ return { context, response: result };
71
+ }
72
+
73
+ return { context, response: items };
74
+ },
75
+ };
76
+ }
77
+
78
+ function applyFiltering(
79
+ items: any[],
80
+ query: Record<string, string>,
81
+ options: FilteringOptions,
82
+ ): any[] {
83
+ const prefix = options.filterPrefix ?? "filter";
84
+ let result = items;
85
+
86
+ for (const field of options.allowed) {
87
+ // Support filter[field]=value format
88
+ const bracketKey = `${prefix}[${field}]`;
89
+ // Support filter.field=value format
90
+ const dotKey = `${prefix}.${field}`;
91
+ // Support plain field=value format as fallback
92
+ const value = query[bracketKey] ?? query[dotKey] ?? query[field];
93
+
94
+ if (value !== undefined) {
95
+ result = result.filter((item) => {
96
+ const itemValue = item[field];
97
+ if (itemValue === undefined) return false;
98
+ return String(itemValue) === value;
99
+ });
100
+ }
101
+ }
102
+
103
+ return result;
104
+ }
105
+
106
+ function applySorting(
107
+ items: any[],
108
+ query: Record<string, string>,
109
+ options: SortingOptions,
110
+ ): any[] {
111
+ const sortParam = options.sortParam ?? "sort";
112
+ const orderParam = options.orderParam ?? "order";
113
+ const sortField = query[sortParam] ?? options.default;
114
+ const rawOrder = query[orderParam] ?? options.defaultOrder ?? "asc";
115
+ const sortOrder = rawOrder === "desc" ? "desc" : "asc";
116
+
117
+ if (!sortField) return items;
118
+
119
+ // Only sort by allowed fields
120
+ if (!options.allowed.includes(sortField)) return items;
121
+
122
+ return items.sort((a, b) => {
123
+ const aVal = a[sortField];
124
+ const bVal = b[sortField];
125
+
126
+ if (aVal === bVal) return 0;
127
+ if (aVal === undefined) return 1;
128
+ if (bVal === undefined) return -1;
129
+
130
+ let comparison: number;
131
+ if (typeof aVal === "number" && typeof bVal === "number") {
132
+ comparison = aVal - bVal;
133
+ } else {
134
+ comparison = String(aVal).localeCompare(String(bVal));
135
+ }
136
+
137
+ return sortOrder === "desc" ? -comparison : comparison;
138
+ });
139
+ }
140
+
141
+ interface PaginatedResult {
142
+ data: any[];
143
+ pagination: {
144
+ page: number;
145
+ limit: number;
146
+ total: number;
147
+ totalPages: number;
148
+ };
149
+ }
150
+
151
+ function applyPagination(
152
+ items: any[],
153
+ query: Record<string, string>,
154
+ options: PaginationOptions,
155
+ ): PaginatedResult {
156
+ const pageParam = options.pageParam ?? "page";
157
+ const limitParam = options.limitParam ?? "limit";
158
+ const defaultLimit = options.defaultLimit ?? 10;
159
+ const maxLimit = options.maxLimit ?? 100;
160
+
161
+ const page = Math.max(1, Number.parseInt(query[pageParam] || "1", 10) || 1);
162
+ const limit = Math.min(
163
+ maxLimit,
164
+ Math.max(
165
+ 1,
166
+ Number.parseInt(query[limitParam] || String(defaultLimit), 10) ||
167
+ defaultLimit,
168
+ ),
169
+ );
170
+
171
+ const total = items.length;
172
+ const totalPages = Math.ceil(total / limit);
173
+ const start = (page - 1) * limit;
174
+ const data = items.slice(start, start + limit);
175
+
176
+ return {
177
+ data,
178
+ pagination: {
179
+ page,
180
+ limit,
181
+ total,
182
+ totalPages,
183
+ },
184
+ };
185
+ }
@@ -0,0 +1,241 @@
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
+ describeFeature(feature, ({ Scenario }) => {
9
+ let mock: any;
10
+ let response: any;
11
+
12
+ // Generate test data
13
+ const generateItems = (count: number) =>
14
+ Array.from({ length: count }, (_, i) => ({ id: i + 1, name: `Item ${i + 1}` }));
15
+
16
+ const namedItems = [
17
+ { id: 1, name: "Charlie" },
18
+ { id: 2, name: "Alice" },
19
+ { id: 3, name: "Bob" },
20
+ ];
21
+
22
+ const categorizedItems = [
23
+ { id: 1, name: "Alice", role: "admin" },
24
+ { id: 2, name: "Bob", role: "user" },
25
+ { id: 3, name: "Charlie", role: "admin" },
26
+ { id: 4, name: "Dave", role: "user" },
27
+ { id: 5, name: "Eve", role: "moderator" },
28
+ ];
29
+
30
+ Scenario("Basic pagination with default settings", ({ Given, When, Then, And }) => {
31
+ Given("I create a mock with 25 items and pagination plugin", () => {
32
+ mock = schmock();
33
+ mock("GET /items", generateItems(25))
34
+ .pipe(queryPlugin({
35
+ pagination: { defaultLimit: 10, maxLimit: 100 },
36
+ }));
37
+ });
38
+
39
+ When("I request page 1", async () => {
40
+ response = await mock.handle("GET", "/items", {
41
+ query: { page: "1" },
42
+ });
43
+ });
44
+
45
+ Then("I should receive {int} items", (_, count: number) => {
46
+ expect(response.body.data).toHaveLength(count);
47
+ });
48
+
49
+ And("the pagination total should be {int}", (_, total: number) => {
50
+ expect(response.body.pagination.total).toBe(total);
51
+ });
52
+
53
+ And("the pagination totalPages should be {int}", (_, totalPages: number) => {
54
+ expect(response.body.pagination.totalPages).toBe(totalPages);
55
+ });
56
+ });
57
+
58
+ Scenario("Pagination with custom limit", ({ Given, When, Then, And }) => {
59
+ Given("I create a mock with 25 items and pagination plugin", () => {
60
+ mock = schmock();
61
+ mock("GET /items", generateItems(25))
62
+ .pipe(queryPlugin({
63
+ pagination: { defaultLimit: 10, maxLimit: 100 },
64
+ }));
65
+ });
66
+
67
+ When("I request page 1 with limit 5", async () => {
68
+ response = await mock.handle("GET", "/items", {
69
+ query: { page: "1", limit: "5" },
70
+ });
71
+ });
72
+
73
+ Then("I should receive {int} items", (_, count: number) => {
74
+ expect(response.body.data).toHaveLength(count);
75
+ });
76
+
77
+ And("the pagination totalPages should be {int}", (_, totalPages: number) => {
78
+ expect(response.body.pagination.totalPages).toBe(totalPages);
79
+ });
80
+ });
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
+ Scenario("Sort items ascending by name", ({ Given, When, Then, And }) => {
107
+ Given("I create a mock with named items and sorting plugin", () => {
108
+ mock = schmock();
109
+ mock("GET /items", namedItems)
110
+ .pipe(queryPlugin({
111
+ sorting: { allowed: ["name", "id"], default: "id" },
112
+ }));
113
+ });
114
+
115
+ When("I request with sort {string} order {string}", async (_, sort: string, order: string) => {
116
+ response = await mock.handle("GET", "/items", {
117
+ query: { sort, order },
118
+ });
119
+ });
120
+
121
+ Then("the first item name should be {string}", (_, name: string) => {
122
+ expect(response.body[0].name).toBe(name);
123
+ });
124
+
125
+ And("the last item name should be {string}", (_, name: string) => {
126
+ expect(response.body[response.body.length - 1].name).toBe(name);
127
+ });
128
+ });
129
+
130
+ Scenario("Sort items descending by name", ({ Given, When, Then, And }) => {
131
+ Given("I create a mock with named items and sorting plugin", () => {
132
+ mock = schmock();
133
+ mock("GET /items", namedItems)
134
+ .pipe(queryPlugin({
135
+ sorting: { allowed: ["name", "id"], default: "id" },
136
+ }));
137
+ });
138
+
139
+ When("I request with sort {string} order {string}", async (_, sort: string, order: string) => {
140
+ response = await mock.handle("GET", "/items", {
141
+ query: { sort, order },
142
+ });
143
+ });
144
+
145
+ Then("the first item name should be {string}", (_, name: string) => {
146
+ expect(response.body[0].name).toBe(name);
147
+ });
148
+
149
+ And("the last item name should be {string}", (_, name: string) => {
150
+ expect(response.body[response.body.length - 1].name).toBe(name);
151
+ });
152
+ });
153
+
154
+ Scenario("Filter items by field value", ({ Given, When, Then, And }) => {
155
+ Given("I create a mock with categorized items and filtering plugin", () => {
156
+ mock = schmock();
157
+ mock("GET /items", categorizedItems)
158
+ .pipe(queryPlugin({
159
+ filtering: { allowed: ["role"] },
160
+ }));
161
+ });
162
+
163
+ When("I request with filter role {string}", async (_, role: string) => {
164
+ response = await mock.handle("GET", "/items", {
165
+ query: { "filter[role]": role },
166
+ });
167
+ });
168
+
169
+ Then("I should receive {int} filtered items", (_, count: number) => {
170
+ expect(response.body).toHaveLength(count);
171
+ });
172
+
173
+ And("all items should have role {string}", (_, role: string) => {
174
+ for (const item of response.body) {
175
+ expect(item.role).toBe(role);
176
+ }
177
+ });
178
+ });
179
+
180
+ Scenario("Combined pagination, sorting, and filtering", ({ Given, When, Then, And }) => {
181
+ const users = [
182
+ { id: 1, name: "Dave", role: "admin" },
183
+ { id: 2, name: "Alice", role: "user" },
184
+ { id: 3, name: "Charlie", role: "admin" },
185
+ { id: 4, name: "Bob", role: "admin" },
186
+ { id: 5, name: "Eve", role: "user" },
187
+ { id: 6, name: "Frank", role: "admin" },
188
+ { id: 7, name: "Grace", role: "user" },
189
+ { id: 8, name: "Hank", role: "admin" },
190
+ { id: 9, name: "Ivy", role: "user" },
191
+ { id: 10, name: "Jack", role: "admin" },
192
+ { id: 11, name: "Karen", role: "user" },
193
+ { id: 12, name: "Leo", role: "admin" },
194
+ { id: 13, name: "Mona", role: "user" },
195
+ { id: 14, name: "Nate", role: "admin" },
196
+ { id: 15, name: "Olivia", role: "user" },
197
+ { id: 16, name: "Paul", role: "admin" },
198
+ { id: 17, name: "Quinn", role: "user" },
199
+ { id: 18, name: "Rose", role: "admin" },
200
+ { id: 19, name: "Sam", role: "user" },
201
+ { id: 20, name: "Tina", role: "admin" },
202
+ ];
203
+
204
+ Given("I create a mock with 20 users and full query plugin", () => {
205
+ mock = schmock();
206
+ mock("GET /users", users)
207
+ .pipe(queryPlugin({
208
+ pagination: { defaultLimit: 10, maxLimit: 100 },
209
+ sorting: { allowed: ["name", "id"] },
210
+ filtering: { allowed: ["role"] },
211
+ }));
212
+ });
213
+
214
+ When("I request page 1 with limit 2 filter role {string} and sort {string}", async (_, role: string, sort: string) => {
215
+ response = await mock.handle("GET", "/users", {
216
+ query: { page: "1", limit: "2", "filter[role]": role, sort },
217
+ });
218
+ });
219
+
220
+ Then("I should receive {int} items", (_, count: number) => {
221
+ expect(response.body.data).toHaveLength(count);
222
+ });
223
+
224
+ And("the items should be sorted by name ascending", () => {
225
+ const names = response.body.data.map((item: any) => item.name);
226
+ const sorted = [...names].sort();
227
+ expect(names).toEqual(sorted);
228
+ });
229
+
230
+ And("all items should have role {string}", (_, role: string) => {
231
+ for (const item of response.body.data) {
232
+ expect(item.role).toBe(role);
233
+ }
234
+ });
235
+
236
+ And("the pagination total should reflect filtered count", () => {
237
+ // 11 admins in the dataset
238
+ expect(response.body.pagination.total).toBe(11);
239
+ });
240
+ });
241
+ });