@sdk-it/spec 0.22.0 → 0.23.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.
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -2629
- package/dist/index.js.map +4 -4
- package/dist/lib/find-polymorphic-varients.d.ts +1 -1
- package/dist/lib/find-polymorphic-varients.d.ts.map +1 -1
- package/dist/lib/find-polymorphic-varients.js +291 -0
- package/dist/lib/find-polymorphic-varients.js.map +7 -0
- package/dist/lib/find-polymorphic-varients.test.js +615 -0
- package/dist/lib/find-polymorphic-varients.test.js.map +7 -0
- package/dist/lib/find-unique-schema-name.js +16 -0
- package/dist/lib/find-unique-schema-name.js.map +7 -0
- package/dist/lib/for-each-operation.d.ts +3 -0
- package/dist/lib/for-each-operation.d.ts.map +1 -0
- package/dist/lib/for-each-operation.js +30 -0
- package/dist/lib/for-each-operation.js.map +7 -0
- package/dist/lib/format-name.js +86 -0
- package/dist/lib/format-name.js.map +7 -0
- package/dist/lib/get-ref-usage.d.ts +1 -1
- package/dist/lib/get-ref-usage.d.ts.map +1 -1
- package/dist/lib/get-ref-usage.js +52 -0
- package/dist/lib/get-ref-usage.js.map +7 -0
- package/dist/lib/is-primitive-schema.js +12 -0
- package/dist/lib/is-primitive-schema.js.map +7 -0
- package/dist/lib/is.d.ts +14 -0
- package/dist/lib/is.d.ts.map +1 -0
- package/dist/lib/is.js +71 -0
- package/dist/lib/is.js.map +7 -0
- package/dist/lib/loaders/load-spec.js +25 -0
- package/dist/lib/loaders/load-spec.js.map +7 -0
- package/dist/lib/loaders/local-loader.js +20 -0
- package/dist/lib/loaders/local-loader.js.map +7 -0
- package/dist/lib/loaders/postman/postman-converter.js +483 -0
- package/dist/lib/loaders/postman/postman-converter.js.map +7 -0
- package/dist/lib/loaders/postman/spec-types.js +1 -0
- package/dist/lib/loaders/postman/spec-types.js.map +7 -0
- package/dist/lib/loaders/remote-loader.js +29 -0
- package/dist/lib/loaders/remote-loader.js.map +7 -0
- package/dist/lib/metadata.js +51 -0
- package/dist/lib/metadata.js.map +7 -0
- package/dist/lib/operation.d.ts +4 -75
- package/dist/lib/operation.d.ts.map +1 -1
- package/dist/lib/operation.js +402 -0
- package/dist/lib/operation.js.map +7 -0
- package/dist/lib/operation.test.js +261 -0
- package/dist/lib/operation.test.js.map +7 -0
- package/dist/lib/options.d.ts +30 -0
- package/dist/lib/options.d.ts.map +1 -0
- package/dist/lib/options.js +54 -0
- package/dist/lib/options.js.map +7 -0
- package/dist/lib/overview-docs.d.ts +4 -0
- package/dist/lib/overview-docs.d.ts.map +1 -0
- package/dist/lib/overview-docs.js +276 -0
- package/dist/lib/overview-docs.js.map +7 -0
- package/dist/lib/pagination/pagination-result.js +235 -0
- package/dist/lib/pagination/pagination-result.js.map +7 -0
- package/dist/lib/pagination/pagination-result.test.js +548 -0
- package/dist/lib/pagination/pagination-result.test.js.map +7 -0
- package/dist/lib/pagination/pagination.d.ts +1 -1
- package/dist/lib/pagination/pagination.d.ts.map +1 -1
- package/dist/lib/pagination/pagination.js +222 -0
- package/dist/lib/pagination/pagination.js.map +7 -0
- package/dist/lib/pagination/pagination.test.js +378 -0
- package/dist/lib/pagination/pagination.test.js.map +7 -0
- package/dist/lib/reserved-keywords.d.ts +6 -0
- package/dist/lib/reserved-keywords.d.ts.map +1 -0
- package/dist/lib/reserved-keywords.js +71 -0
- package/dist/lib/reserved-keywords.js.map +7 -0
- package/dist/lib/security.d.ts +6 -4
- package/dist/lib/security.d.ts.map +1 -1
- package/dist/lib/security.js +75 -0
- package/dist/lib/security.js.map +7 -0
- package/dist/lib/sidebar.d.ts +5 -3
- package/dist/lib/sidebar.d.ts.map +1 -1
- package/dist/lib/sidebar.js +79 -0
- package/dist/lib/sidebar.js.map +7 -0
- package/dist/lib/tag.d.ts +19 -0
- package/dist/lib/tag.d.ts.map +1 -0
- package/dist/lib/tag.js +132 -0
- package/dist/lib/tag.js.map +7 -0
- package/dist/lib/tune.d.ts +1 -1
- package/dist/lib/tune.d.ts.map +1 -1
- package/dist/lib/tune.js +291 -0
- package/dist/lib/tune.js.map +7 -0
- package/dist/lib/tune.test.js +60 -0
- package/dist/lib/tune.test.js.map +7 -0
- package/dist/lib/types.d.ts +37 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +1 -0
- package/dist/lib/types.js.map +7 -0
- package/package.json +4 -3
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { isRef } from "@sdk-it/core/ref.js";
|
|
2
|
+
import { isEmpty } from "@sdk-it/core/utils.js";
|
|
3
|
+
import { getHasMoreName, getItemsName } from "./pagination-result.js";
|
|
4
|
+
const OFFSET_PARAM_REGEXES = [
|
|
5
|
+
/\boffset\b/i,
|
|
6
|
+
/\bskip\b/i,
|
|
7
|
+
/\bstart(?:ing_at|_index)?\b/i,
|
|
8
|
+
// e.g., start, starting_at, start_index
|
|
9
|
+
/\bfrom\b/i
|
|
10
|
+
];
|
|
11
|
+
const GENERIC_LIMIT_PARAM_REGEXES = [
|
|
12
|
+
/\blimit\b/i,
|
|
13
|
+
/\bcount\b/i,
|
|
14
|
+
/\b(?:page_?)?size\b/i,
|
|
15
|
+
// e.g., size, page_size, pagesize
|
|
16
|
+
/\bmax_results\b/i,
|
|
17
|
+
/\bnum_results\b/i,
|
|
18
|
+
/\bshow\b/i,
|
|
19
|
+
// Can sometimes mean limit
|
|
20
|
+
/\bper_?page\b/i,
|
|
21
|
+
// e.g., per_page, perpage
|
|
22
|
+
/\bper-page\b/i,
|
|
23
|
+
/\btake\b/i
|
|
24
|
+
];
|
|
25
|
+
const PAGE_NUMBER_REGEXES = [
|
|
26
|
+
// /^(currentPage)?page$/i, // Exact match for "page"
|
|
27
|
+
// /^currentPage$/i, // Exact match for "currentPage"
|
|
28
|
+
/^p$/i,
|
|
29
|
+
// Exact match for "p" (common shorthand)
|
|
30
|
+
// /\bpage_?(?:number|no|num|idx|index)\b/i, // e.g., page_number, pageNumber, page_num, page_idx
|
|
31
|
+
/^(current)?_?page(_?)(?:number|no|num|idx|index)?\b/i
|
|
32
|
+
];
|
|
33
|
+
const PAGE_SIZE_REGEXES = [
|
|
34
|
+
/\bpage_?size\b/i,
|
|
35
|
+
// e.g., page_size, pagesize
|
|
36
|
+
/^size$/i,
|
|
37
|
+
// Exact "size"
|
|
38
|
+
// /\bsize\b/i, // Broader "size" - can be ambiguous, prefer more specific ones first
|
|
39
|
+
/\blimit\b/i,
|
|
40
|
+
// Limit is often used for page size
|
|
41
|
+
/\bcount\b/i,
|
|
42
|
+
// Count can also be used for page size
|
|
43
|
+
/\bper_?page\b/i,
|
|
44
|
+
// e.g., per_page, perpage
|
|
45
|
+
/\bper-page\b/i,
|
|
46
|
+
/\bnum_?(?:items|records|results)\b/i,
|
|
47
|
+
// e.g., num_items, numitems
|
|
48
|
+
/\bresults_?per_?page\b/i
|
|
49
|
+
];
|
|
50
|
+
const CURSOR_REGEXES = [
|
|
51
|
+
/\bmarker\b/i,
|
|
52
|
+
/\bcursor\b/i,
|
|
53
|
+
/\bafter(?:_?cursor)?\b/i,
|
|
54
|
+
// e.g., after, after_cursor
|
|
55
|
+
/\bbefore(?:_?cursor)?\b/i,
|
|
56
|
+
// e.g., before, before_cursor
|
|
57
|
+
/\b(next|prev|previous)_?(?:page_?)?token\b/i,
|
|
58
|
+
// e.g., next_page_token, nextPageToken, prev_token
|
|
59
|
+
/\b(next|prev|previous)_?cursor\b/i,
|
|
60
|
+
// e.g., next_cursor, previousCursor
|
|
61
|
+
/\bcontinuation(?:_?token)?\b/i,
|
|
62
|
+
// e.g., continuation, continuation_token
|
|
63
|
+
/\bpage(?:_?(token|id))?\b/i,
|
|
64
|
+
// e.g., after, after_cursor
|
|
65
|
+
/\bstart_?(?:key|cursor|token|after)\b/i
|
|
66
|
+
// e.g., start_key, startCursor, startToken, startAfter
|
|
67
|
+
];
|
|
68
|
+
const CURSOR_LIMIT_REGEXES = [
|
|
69
|
+
/\blimit\b/i,
|
|
70
|
+
/\bcount\b/i,
|
|
71
|
+
/\bsize\b/i,
|
|
72
|
+
// General size
|
|
73
|
+
/\bfirst\b/i,
|
|
74
|
+
// Common in Relay-style cursor pagination (forward pagination)
|
|
75
|
+
/\blast\b/i,
|
|
76
|
+
// Common in Relay-style cursor pagination (backward pagination)
|
|
77
|
+
/\bpage_?size\b/i,
|
|
78
|
+
// Sometimes page_size is used with cursors
|
|
79
|
+
/\bnum_?(?:items|records|results)\b/i,
|
|
80
|
+
// e.g., num_items
|
|
81
|
+
/\bmax_?items\b/i,
|
|
82
|
+
/\btake\b/i
|
|
83
|
+
];
|
|
84
|
+
function findParamAndKeyword(queryParams, regexes, excludeParamName) {
|
|
85
|
+
for (const param of queryParams) {
|
|
86
|
+
if (param.name === excludeParamName) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
for (const regex of regexes) {
|
|
90
|
+
const match = param.name.match(regex);
|
|
91
|
+
if (match) {
|
|
92
|
+
return { param, keyword: match[0] };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
function coearceParameters(parameters) {
|
|
99
|
+
const cleanedParameters = [];
|
|
100
|
+
for (const param of parameters) {
|
|
101
|
+
if (!param.schema) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (isRef(param.schema)) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (!param.schema.type) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (param.schema) {
|
|
111
|
+
cleanedParameters.push({
|
|
112
|
+
...param,
|
|
113
|
+
schema: {
|
|
114
|
+
...param.schema,
|
|
115
|
+
type: Array.isArray(param.schema.type) ? param.schema.type : [param.schema.type]
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return cleanedParameters;
|
|
121
|
+
}
|
|
122
|
+
function isOffsetPagination(operation, parameters) {
|
|
123
|
+
const params = coearceParameters(parameters).filter(
|
|
124
|
+
(it) => it.schema.type.includes("integer") || it.schema.type.includes("number")
|
|
125
|
+
);
|
|
126
|
+
const offsetMatch = findParamAndKeyword(params, OFFSET_PARAM_REGEXES);
|
|
127
|
+
if (!offsetMatch) return null;
|
|
128
|
+
const limitMatch = findParamAndKeyword(
|
|
129
|
+
params,
|
|
130
|
+
GENERIC_LIMIT_PARAM_REGEXES,
|
|
131
|
+
offsetMatch.param.name
|
|
132
|
+
);
|
|
133
|
+
if (!limitMatch) return null;
|
|
134
|
+
return {
|
|
135
|
+
type: "offset",
|
|
136
|
+
offsetParamName: offsetMatch.param.name,
|
|
137
|
+
offsetKeyword: offsetMatch.keyword,
|
|
138
|
+
limitParamName: limitMatch.param.name,
|
|
139
|
+
limitKeyword: limitMatch.keyword
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function isPagePagination(operation, parameters) {
|
|
143
|
+
const params = coearceParameters(parameters).filter(
|
|
144
|
+
(it) => it.schema.type.includes("integer") || it.schema.type.includes("number")
|
|
145
|
+
);
|
|
146
|
+
if (params.length < 2) return null;
|
|
147
|
+
const pageNoMatch = findParamAndKeyword(params, PAGE_NUMBER_REGEXES);
|
|
148
|
+
if (!pageNoMatch) return null;
|
|
149
|
+
const pageSizeMatch = findParamAndKeyword(
|
|
150
|
+
params,
|
|
151
|
+
PAGE_SIZE_REGEXES,
|
|
152
|
+
pageNoMatch.param.name
|
|
153
|
+
);
|
|
154
|
+
if (!pageSizeMatch) return null;
|
|
155
|
+
return {
|
|
156
|
+
type: "page",
|
|
157
|
+
pageNumberParamName: pageNoMatch.param.name,
|
|
158
|
+
pageNumberKeyword: pageNoMatch.keyword,
|
|
159
|
+
pageSizeParamName: pageSizeMatch.param.name,
|
|
160
|
+
pageSizeKeyword: pageSizeMatch.keyword
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function isCursorPagination(operation, parameters = []) {
|
|
164
|
+
const queryParams = parameters;
|
|
165
|
+
if (queryParams.length < 2) return null;
|
|
166
|
+
const cursorMatch = findParamAndKeyword(queryParams, CURSOR_REGEXES);
|
|
167
|
+
if (!cursorMatch) return null;
|
|
168
|
+
const limitMatch = findParamAndKeyword(
|
|
169
|
+
queryParams,
|
|
170
|
+
CURSOR_LIMIT_REGEXES,
|
|
171
|
+
cursorMatch.param.name
|
|
172
|
+
);
|
|
173
|
+
if (!limitMatch) return null;
|
|
174
|
+
return {
|
|
175
|
+
type: "cursor",
|
|
176
|
+
cursorParamName: cursorMatch.param.name,
|
|
177
|
+
cursorKeyword: cursorMatch.keyword,
|
|
178
|
+
limitParamName: limitMatch.param.name,
|
|
179
|
+
limitKeyword: limitMatch.keyword
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function guessPagination(operation, body, response) {
|
|
183
|
+
const bodyParameters = body && body.properties ? Object.entries(body.properties).map(([it, schema]) => ({
|
|
184
|
+
name: it,
|
|
185
|
+
schema
|
|
186
|
+
})) : [];
|
|
187
|
+
const parameters = operation.parameters;
|
|
188
|
+
if (isEmpty(operation.parameters) && isEmpty(bodyParameters)) {
|
|
189
|
+
return { type: "none", reason: "no parameters" };
|
|
190
|
+
}
|
|
191
|
+
if (!response) {
|
|
192
|
+
return { type: "none", reason: "no response" };
|
|
193
|
+
}
|
|
194
|
+
if (!response.properties) {
|
|
195
|
+
return { type: "none", reason: "empty response" };
|
|
196
|
+
}
|
|
197
|
+
const properties = response.properties;
|
|
198
|
+
const itemsKey = getItemsName(properties);
|
|
199
|
+
if (!itemsKey) {
|
|
200
|
+
return { type: "none", reason: "no items key" };
|
|
201
|
+
}
|
|
202
|
+
const hasMoreKey = getHasMoreName(excludeKey(properties, itemsKey));
|
|
203
|
+
if (!hasMoreKey) {
|
|
204
|
+
return { type: "none", reason: "no hasMore key" };
|
|
205
|
+
}
|
|
206
|
+
const pagination = isOffsetPagination(operation, [...parameters, ...bodyParameters]) || isPagePagination(operation, [...parameters, ...bodyParameters]) || isCursorPagination(operation, [...parameters, ...bodyParameters]);
|
|
207
|
+
return pagination ? { ...pagination, items: itemsKey, hasMore: hasMoreKey } : { type: "none", reason: "no pagination" };
|
|
208
|
+
}
|
|
209
|
+
function excludeKey(obj, key) {
|
|
210
|
+
const { [key]: _, ...rest } = obj;
|
|
211
|
+
return rest;
|
|
212
|
+
}
|
|
213
|
+
export {
|
|
214
|
+
CURSOR_LIMIT_REGEXES,
|
|
215
|
+
CURSOR_REGEXES,
|
|
216
|
+
GENERIC_LIMIT_PARAM_REGEXES,
|
|
217
|
+
OFFSET_PARAM_REGEXES,
|
|
218
|
+
PAGE_NUMBER_REGEXES,
|
|
219
|
+
PAGE_SIZE_REGEXES,
|
|
220
|
+
guessPagination
|
|
221
|
+
};
|
|
222
|
+
//# sourceMappingURL=pagination.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/pagination/pagination.ts"],
|
|
4
|
+
"sourcesContent": ["import type {\n ReferenceObject,\n SchemaObject,\n SchemasObject,\n} from 'openapi3-ts/oas31';\n\nimport { isRef } from '@sdk-it/core/ref.js';\nimport { isEmpty } from '@sdk-it/core/utils.js';\n\nimport type { TunedOperationObject } from '../types.js';\nimport { getHasMoreName, getItemsName } from './pagination-result.js';\n\ninterface PaginationResultBase {\n type: 'offset' | 'page' | 'cursor' | 'none';\n}\n\nexport interface OffsetPaginationResult extends PaginationResultBase {\n type: 'offset';\n offsetParamName: string;\n offsetKeyword: string; // The actual part of param name that matched\n limitParamName: string;\n limitKeyword: string; // The actual part of param name that matched\n}\n\nexport interface PagePaginationResult extends PaginationResultBase {\n type: 'page';\n pageNumberParamName: string;\n pageNumberKeyword: string;\n pageSizeParamName: string;\n pageSizeKeyword: string;\n}\n\nexport interface CursorPaginationResult extends PaginationResultBase {\n type: 'cursor';\n cursorParamName: string;\n cursorKeyword: string;\n limitParamName: string;\n limitKeyword: string;\n}\n\nexport interface NoPaginationResult extends PaginationResultBase {\n type: 'none';\n reason: string;\n}\n\nexport type PaginationGuess =\n | ((\n | OffsetPaginationResult\n | PagePaginationResult\n | CursorPaginationResult\n ) & { items: string; hasMore: string })\n | NoPaginationResult;\n\n// --- Keyword Regex Definitions ---\n\nexport const OFFSET_PARAM_REGEXES: RegExp[] = [\n /\\boffset\\b/i,\n /\\bskip\\b/i,\n /\\bstart(?:ing_at|_index)?\\b/i, // e.g., start, starting_at, start_index\n /\\bfrom\\b/i,\n];\n\nexport const GENERIC_LIMIT_PARAM_REGEXES: RegExp[] = [\n /\\blimit\\b/i,\n /\\bcount\\b/i,\n /\\b(?:page_?)?size\\b/i, // e.g., size, page_size, pagesize\n /\\bmax_results\\b/i,\n /\\bnum_results\\b/i,\n /\\bshow\\b/i, // Can sometimes mean limit\n /\\bper_?page\\b/i, // e.g., per_page, perpage\n /\\bper-page\\b/i,\n /\\btake\\b/i,\n];\n\nexport const PAGE_NUMBER_REGEXES: RegExp[] = [\n // /^(currentPage)?page$/i, // Exact match for \"page\"\n // /^currentPage$/i, // Exact match for \"currentPage\"\n /^p$/i, // Exact match for \"p\" (common shorthand)\n // /\\bpage_?(?:number|no|num|idx|index)\\b/i, // e.g., page_number, pageNumber, page_num, page_idx\n /^(current)?_?page(_?)(?:number|no|num|idx|index)?\\b/i,\n];\n\n// Regexes for parameters indicating page size (when used with page number)\nexport const PAGE_SIZE_REGEXES: RegExp[] = [\n /\\bpage_?size\\b/i, // e.g., page_size, pagesize\n /^size$/i, // Exact \"size\"\n // /\\bsize\\b/i, // Broader \"size\" - can be ambiguous, prefer more specific ones first\n /\\blimit\\b/i, // Limit is often used for page size\n /\\bcount\\b/i, // Count can also be used for page size\n /\\bper_?page\\b/i, // e.g., per_page, perpage\n /\\bper-page\\b/i,\n /\\bnum_?(?:items|records|results)\\b/i, // e.g., num_items, numitems\n /\\bresults_?per_?page\\b/i,\n];\n\n// Regexes for parameters indicating a cursor\nexport const CURSOR_REGEXES: RegExp[] = [\n /\\bmarker\\b/i,\n /\\bcursor\\b/i,\n /\\bafter(?:_?cursor)?\\b/i, // e.g., after, after_cursor\n /\\bbefore(?:_?cursor)?\\b/i, // e.g., before, before_cursor\n /\\b(next|prev|previous)_?(?:page_?)?token\\b/i, // e.g., next_page_token, nextPageToken, prev_token\n /\\b(next|prev|previous)_?cursor\\b/i, // e.g., next_cursor, previousCursor\n /\\bcontinuation(?:_?token)?\\b/i, // e.g., continuation, continuation_token\n /\\bpage(?:_?(token|id))?\\b/i, // e.g., after, after_cursor\n\n /\\bstart_?(?:key|cursor|token|after)\\b/i, // e.g., start_key, startCursor, startToken, startAfter\n];\n\n// Regexes for parameters indicating a limit when used with cursors\nexport const CURSOR_LIMIT_REGEXES: RegExp[] = [\n /\\blimit\\b/i,\n /\\bcount\\b/i,\n /\\bsize\\b/i, // General size\n /\\bfirst\\b/i, // Common in Relay-style cursor pagination (forward pagination)\n /\\blast\\b/i, // Common in Relay-style cursor pagination (backward pagination)\n /\\bpage_?size\\b/i, // Sometimes page_size is used with cursors\n /\\bnum_?(?:items|records|results)\\b/i, // e.g., num_items\n /\\bmax_?items\\b/i,\n /\\btake\\b/i,\n];\n\n// --- Helper Function ---\nfunction findParamAndKeyword(\n queryParams: { name: string }[],\n regexes: RegExp[],\n excludeParamName?: string,\n) {\n for (const param of queryParams) {\n if (param.name === excludeParamName) {\n continue;\n }\n for (const regex of regexes) {\n const match = param.name.match(regex);\n if (match) {\n return { param, keyword: match[0] }; // match[0] is the actual matched substring\n }\n }\n }\n return null;\n}\n\nfunction coearceParameters(\n parameters: { name: string; schema?: SchemaObject | ReferenceObject }[],\n) {\n const cleanedParameters: {\n name: string;\n schema: Omit<SchemaObject, 'type'> & {\n type: string[]; // Ensure type is always an array\n };\n }[] = [];\n for (const param of parameters) {\n if (!param.schema) {\n continue;\n }\n if (isRef(param.schema)) {\n continue;\n }\n if (!param.schema.type) {\n continue;\n }\n if (param.schema) {\n cleanedParameters.push({\n ...param,\n schema: {\n ...param.schema,\n type: Array.isArray(param.schema.type)\n ? param.schema.type\n : [param.schema.type],\n },\n });\n }\n }\n return cleanedParameters;\n}\n\nfunction isOffsetPagination(\n operation: TunedOperationObject,\n parameters: { name: string; schema?: SchemaObject | ReferenceObject }[],\n): OffsetPaginationResult | null {\n const params = coearceParameters(parameters).filter(\n (it) =>\n it.schema.type.includes('integer') || it.schema.type.includes('number'),\n );\n const offsetMatch = findParamAndKeyword(params, OFFSET_PARAM_REGEXES);\n\n if (!offsetMatch) return null;\n\n const limitMatch = findParamAndKeyword(\n params,\n GENERIC_LIMIT_PARAM_REGEXES,\n offsetMatch.param.name,\n );\n if (!limitMatch) return null;\n\n return {\n type: 'offset',\n offsetParamName: offsetMatch.param.name,\n offsetKeyword: offsetMatch.keyword,\n limitParamName: limitMatch.param.name,\n limitKeyword: limitMatch.keyword,\n };\n}\n\nfunction isPagePagination(\n operation: TunedOperationObject,\n parameters: { name: string; schema?: SchemaObject | ReferenceObject }[],\n): PagePaginationResult | null {\n const params = coearceParameters(parameters).filter(\n (it) =>\n it.schema.type.includes('integer') || it.schema.type.includes('number'),\n );\n\n if (params.length < 2) return null;\n\n const pageNoMatch = findParamAndKeyword(params, PAGE_NUMBER_REGEXES);\n if (!pageNoMatch) return null;\n\n const pageSizeMatch = findParamAndKeyword(\n params,\n PAGE_SIZE_REGEXES,\n pageNoMatch.param.name,\n );\n if (!pageSizeMatch) return null;\n\n return {\n type: 'page',\n pageNumberParamName: pageNoMatch.param.name,\n pageNumberKeyword: pageNoMatch.keyword,\n pageSizeParamName: pageSizeMatch.param.name,\n pageSizeKeyword: pageSizeMatch.keyword,\n };\n}\n\nfunction isCursorPagination(\n operation: TunedOperationObject,\n parameters: { name: string; schema?: SchemaObject | ReferenceObject }[] = [],\n): CursorPaginationResult | null {\n const queryParams = parameters;\n if (queryParams.length < 2) return null; // Need at least a cursor and a limit-like param\n\n const cursorMatch = findParamAndKeyword(queryParams, CURSOR_REGEXES);\n if (!cursorMatch) return null;\n\n const limitMatch = findParamAndKeyword(\n queryParams,\n CURSOR_LIMIT_REGEXES,\n cursorMatch.param.name,\n );\n if (!limitMatch) return null;\n\n return {\n type: 'cursor',\n cursorParamName: cursorMatch.param.name,\n cursorKeyword: cursorMatch.keyword,\n limitParamName: limitMatch.param.name,\n limitKeyword: limitMatch.keyword,\n };\n}\n\n/**\n * Guesses the pagination strategy of an OpenAPI operation based on its query parameters.\n * It checks for offset, page-based, and cursor-based pagination in that order.\n *\n * @param operation The OpenAPI operation object.\n * @returns A PaginationGuess object indicating the detected type and relevant parameters.\n */\nexport function guessPagination(\n operation: TunedOperationObject,\n body?: SchemaObject,\n response?: SchemaObject,\n): PaginationGuess {\n const bodyParameters =\n body && body.properties\n ? Object.entries(body.properties).map(([it, schema]) => ({\n name: it,\n schema,\n }))\n : [];\n const parameters = operation.parameters;\n\n if (isEmpty(operation.parameters) && isEmpty(bodyParameters)) {\n return { type: 'none', reason: 'no parameters' };\n }\n if (!response) {\n return { type: 'none', reason: 'no response' };\n }\n if (!response.properties) {\n return { type: 'none', reason: 'empty response' };\n }\n const properties = response.properties as SchemasObject;\n\n const itemsKey = getItemsName(properties);\n if (!itemsKey) {\n return { type: 'none', reason: 'no items key' };\n }\n const hasMoreKey = getHasMoreName(excludeKey(properties, itemsKey));\n\n if (!hasMoreKey) {\n return { type: 'none', reason: 'no hasMore key' };\n }\n const pagination =\n isOffsetPagination(operation, [...parameters, ...bodyParameters]) ||\n isPagePagination(operation, [...parameters, ...bodyParameters]) ||\n isCursorPagination(operation, [...parameters, ...bodyParameters]);\n return pagination\n ? { ...pagination, items: itemsKey, hasMore: hasMoreKey }\n : { type: 'none', reason: 'no pagination' };\n}\n\nfunction excludeKey<T extends Record<string, any>>(\n obj: T,\n key: string,\n): Omit<T, typeof key> {\n const { [key]: _, ...rest } = obj;\n return rest;\n}\n"],
|
|
5
|
+
"mappings": "AAMA,SAAS,aAAa;AACtB,SAAS,eAAe;AAGxB,SAAS,gBAAgB,oBAAoB;AA6CtC,MAAM,uBAAiC;AAAA,EAC5C;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EACA;AACF;AAEO,MAAM,8BAAwC;AAAA,EACnD;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA,EACA;AACF;AAEO,MAAM,sBAAgC;AAAA;AAAA;AAAA,EAG3C;AAAA;AAAA;AAAA,EAEA;AACF;AAGO,MAAM,oBAA8B;AAAA,EACzC;AAAA;AAAA,EACA;AAAA;AAAA;AAAA,EAEA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EACA;AACF;AAGO,MAAM,iBAA2B;AAAA,EACtC;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA;AACF;AAGO,MAAM,uBAAiC;AAAA,EAC5C;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA,EACA;AACF;AAGA,SAAS,oBACP,aACA,SACA,kBACA;AACA,aAAW,SAAS,aAAa;AAC/B,QAAI,MAAM,SAAS,kBAAkB;AACnC;AAAA,IACF;AACA,eAAW,SAAS,SAAS;AAC3B,YAAM,QAAQ,MAAM,KAAK,MAAM,KAAK;AACpC,UAAI,OAAO;AACT,eAAO,EAAE,OAAO,SAAS,MAAM,CAAC,EAAE;AAAA,MACpC;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,kBACP,YACA;AACA,QAAM,oBAKA,CAAC;AACP,aAAW,SAAS,YAAY;AAC9B,QAAI,CAAC,MAAM,QAAQ;AACjB;AAAA,IACF;AACA,QAAI,MAAM,MAAM,MAAM,GAAG;AACvB;AAAA,IACF;AACA,QAAI,CAAC,MAAM,OAAO,MAAM;AACtB;AAAA,IACF;AACA,QAAI,MAAM,QAAQ;AAChB,wBAAkB,KAAK;AAAA,QACrB,GAAG;AAAA,QACH,QAAQ;AAAA,UACN,GAAG,MAAM;AAAA,UACT,MAAM,MAAM,QAAQ,MAAM,OAAO,IAAI,IACjC,MAAM,OAAO,OACb,CAAC,MAAM,OAAO,IAAI;AAAA,QACxB;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,mBACP,WACA,YAC+B;AAC/B,QAAM,SAAS,kBAAkB,UAAU,EAAE;AAAA,IAC3C,CAAC,OACC,GAAG,OAAO,KAAK,SAAS,SAAS,KAAK,GAAG,OAAO,KAAK,SAAS,QAAQ;AAAA,EAC1E;AACA,QAAM,cAAc,oBAAoB,QAAQ,oBAAoB;AAEpE,MAAI,CAAC,YAAa,QAAO;AAEzB,QAAM,aAAa;AAAA,IACjB;AAAA,IACA;AAAA,IACA,YAAY,MAAM;AAAA,EACpB;AACA,MAAI,CAAC,WAAY,QAAO;AAExB,SAAO;AAAA,IACL,MAAM;AAAA,IACN,iBAAiB,YAAY,MAAM;AAAA,IACnC,eAAe,YAAY;AAAA,IAC3B,gBAAgB,WAAW,MAAM;AAAA,IACjC,cAAc,WAAW;AAAA,EAC3B;AACF;AAEA,SAAS,iBACP,WACA,YAC6B;AAC7B,QAAM,SAAS,kBAAkB,UAAU,EAAE;AAAA,IAC3C,CAAC,OACC,GAAG,OAAO,KAAK,SAAS,SAAS,KAAK,GAAG,OAAO,KAAK,SAAS,QAAQ;AAAA,EAC1E;AAEA,MAAI,OAAO,SAAS,EAAG,QAAO;AAE9B,QAAM,cAAc,oBAAoB,QAAQ,mBAAmB;AACnE,MAAI,CAAC,YAAa,QAAO;AAEzB,QAAM,gBAAgB;AAAA,IACpB;AAAA,IACA;AAAA,IACA,YAAY,MAAM;AAAA,EACpB;AACA,MAAI,CAAC,cAAe,QAAO;AAE3B,SAAO;AAAA,IACL,MAAM;AAAA,IACN,qBAAqB,YAAY,MAAM;AAAA,IACvC,mBAAmB,YAAY;AAAA,IAC/B,mBAAmB,cAAc,MAAM;AAAA,IACvC,iBAAiB,cAAc;AAAA,EACjC;AACF;AAEA,SAAS,mBACP,WACA,aAA0E,CAAC,GAC5C;AAC/B,QAAM,cAAc;AACpB,MAAI,YAAY,SAAS,EAAG,QAAO;AAEnC,QAAM,cAAc,oBAAoB,aAAa,cAAc;AACnE,MAAI,CAAC,YAAa,QAAO;AAEzB,QAAM,aAAa;AAAA,IACjB;AAAA,IACA;AAAA,IACA,YAAY,MAAM;AAAA,EACpB;AACA,MAAI,CAAC,WAAY,QAAO;AAExB,SAAO;AAAA,IACL,MAAM;AAAA,IACN,iBAAiB,YAAY,MAAM;AAAA,IACnC,eAAe,YAAY;AAAA,IAC3B,gBAAgB,WAAW,MAAM;AAAA,IACjC,cAAc,WAAW;AAAA,EAC3B;AACF;AASO,SAAS,gBACd,WACA,MACA,UACiB;AACjB,QAAM,iBACJ,QAAQ,KAAK,aACT,OAAO,QAAQ,KAAK,UAAU,EAAE,IAAI,CAAC,CAAC,IAAI,MAAM,OAAO;AAAA,IACrD,MAAM;AAAA,IACN;AAAA,EACF,EAAE,IACF,CAAC;AACP,QAAM,aAAa,UAAU;AAE7B,MAAI,QAAQ,UAAU,UAAU,KAAK,QAAQ,cAAc,GAAG;AAC5D,WAAO,EAAE,MAAM,QAAQ,QAAQ,gBAAgB;AAAA,EACjD;AACA,MAAI,CAAC,UAAU;AACb,WAAO,EAAE,MAAM,QAAQ,QAAQ,cAAc;AAAA,EAC/C;AACA,MAAI,CAAC,SAAS,YAAY;AACxB,WAAO,EAAE,MAAM,QAAQ,QAAQ,iBAAiB;AAAA,EAClD;AACA,QAAM,aAAa,SAAS;AAE5B,QAAM,WAAW,aAAa,UAAU;AACxC,MAAI,CAAC,UAAU;AACb,WAAO,EAAE,MAAM,QAAQ,QAAQ,eAAe;AAAA,EAChD;AACA,QAAM,aAAa,eAAe,WAAW,YAAY,QAAQ,CAAC;AAElE,MAAI,CAAC,YAAY;AACf,WAAO,EAAE,MAAM,QAAQ,QAAQ,iBAAiB;AAAA,EAClD;AACA,QAAM,aACJ,mBAAmB,WAAW,CAAC,GAAG,YAAY,GAAG,cAAc,CAAC,KAChE,iBAAiB,WAAW,CAAC,GAAG,YAAY,GAAG,cAAc,CAAC,KAC9D,mBAAmB,WAAW,CAAC,GAAG,YAAY,GAAG,cAAc,CAAC;AAClE,SAAO,aACH,EAAE,GAAG,YAAY,OAAO,UAAU,SAAS,WAAW,IACtD,EAAE,MAAM,QAAQ,QAAQ,gBAAgB;AAC9C;AAEA,SAAS,WACP,KACA,KACqB;AACrB,QAAM,EAAE,CAAC,GAAG,GAAG,GAAG,GAAG,KAAK,IAAI;AAC9B,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
CURSOR_LIMIT_REGEXES,
|
|
5
|
+
CURSOR_REGEXES,
|
|
6
|
+
GENERIC_LIMIT_PARAM_REGEXES,
|
|
7
|
+
OFFSET_PARAM_REGEXES,
|
|
8
|
+
PAGE_NUMBER_REGEXES,
|
|
9
|
+
PAGE_SIZE_REGEXES,
|
|
10
|
+
guessPagination
|
|
11
|
+
} from "./pagination.ts";
|
|
12
|
+
function createOperation(params) {
|
|
13
|
+
return {
|
|
14
|
+
tags: ["test"],
|
|
15
|
+
operationId: "testOperation",
|
|
16
|
+
parameters: params,
|
|
17
|
+
requestBody: { content: {} },
|
|
18
|
+
responses: {}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function qParam(name) {
|
|
22
|
+
return {
|
|
23
|
+
name,
|
|
24
|
+
in: "query",
|
|
25
|
+
required: false,
|
|
26
|
+
schema: {
|
|
27
|
+
type: "string"
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function hParam(name) {
|
|
32
|
+
return {
|
|
33
|
+
name,
|
|
34
|
+
in: "header",
|
|
35
|
+
required: false,
|
|
36
|
+
schema: {
|
|
37
|
+
type: "string"
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function pParam(name) {
|
|
42
|
+
return {
|
|
43
|
+
name,
|
|
44
|
+
in: "path",
|
|
45
|
+
required: false,
|
|
46
|
+
schema: {
|
|
47
|
+
type: "string"
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function getExpectedKeyword(paramName, regexArray) {
|
|
52
|
+
for (const regex of regexArray) {
|
|
53
|
+
const match = paramName.match(regex);
|
|
54
|
+
if (match) {
|
|
55
|
+
return match[0];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return paramName;
|
|
59
|
+
}
|
|
60
|
+
describe("guessPagination()", () => {
|
|
61
|
+
describe("No Pagination Detected", () => {
|
|
62
|
+
test('should return "none" for an operation with an empty parameters array', () => {
|
|
63
|
+
const op = createOperation([]);
|
|
64
|
+
assert.deepStrictEqual(guessPagination(op), { type: "none" });
|
|
65
|
+
});
|
|
66
|
+
test('should return "none" for an operation with undefined parameters', () => {
|
|
67
|
+
const op = createOperation(void 0);
|
|
68
|
+
assert.deepStrictEqual(guessPagination(op), { type: "none" });
|
|
69
|
+
});
|
|
70
|
+
test('should return "none" for non-pagination query parameters', () => {
|
|
71
|
+
const op = createOperation([qParam("search"), qParam("filterBy")]);
|
|
72
|
+
assert.deepStrictEqual(guessPagination(op), { type: "none" });
|
|
73
|
+
});
|
|
74
|
+
test('should return "none" if relevant parameters are not in "query"', () => {
|
|
75
|
+
const op = createOperation([hParam("offset"), hParam("limit")]);
|
|
76
|
+
assert.deepStrictEqual(guessPagination(op), { type: "none" });
|
|
77
|
+
});
|
|
78
|
+
test('should return "none" if only one part of a pair is present (e.g., only offset)', () => {
|
|
79
|
+
assert.deepStrictEqual(
|
|
80
|
+
guessPagination(createOperation([qParam("offset")])),
|
|
81
|
+
{ type: "none" },
|
|
82
|
+
"Only offset"
|
|
83
|
+
);
|
|
84
|
+
assert.deepStrictEqual(
|
|
85
|
+
guessPagination(createOperation([qParam("page")])),
|
|
86
|
+
{ type: "none" },
|
|
87
|
+
"Only page"
|
|
88
|
+
);
|
|
89
|
+
assert.deepStrictEqual(
|
|
90
|
+
guessPagination(createOperation([qParam("cursor")])),
|
|
91
|
+
{ type: "none" },
|
|
92
|
+
"Only cursor"
|
|
93
|
+
);
|
|
94
|
+
assert.deepStrictEqual(
|
|
95
|
+
guessPagination(createOperation([qParam("limit")])),
|
|
96
|
+
{ type: "none" },
|
|
97
|
+
"Only limit"
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
test('should return "none" for mixed non-query and insufficient query params', () => {
|
|
101
|
+
const op = createOperation([
|
|
102
|
+
pParam("userId"),
|
|
103
|
+
qParam("offset"),
|
|
104
|
+
hParam("X-Request-ID")
|
|
105
|
+
]);
|
|
106
|
+
assert.deepStrictEqual(guessPagination(op), { type: "none" });
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
describe("Offset Pagination Detection", () => {
|
|
110
|
+
const testOffsetKeywords = [
|
|
111
|
+
"offset",
|
|
112
|
+
"skip",
|
|
113
|
+
"start",
|
|
114
|
+
"start_index",
|
|
115
|
+
"starting_at",
|
|
116
|
+
"from",
|
|
117
|
+
"Offset",
|
|
118
|
+
"SKIP"
|
|
119
|
+
];
|
|
120
|
+
const testLimitKeywords = [
|
|
121
|
+
"limit",
|
|
122
|
+
"count",
|
|
123
|
+
"size",
|
|
124
|
+
"page_size",
|
|
125
|
+
"pageSize",
|
|
126
|
+
"max_results",
|
|
127
|
+
"take",
|
|
128
|
+
"Limit",
|
|
129
|
+
"COUNT"
|
|
130
|
+
];
|
|
131
|
+
for (const offsetKey of testOffsetKeywords) {
|
|
132
|
+
for (const limitKey of testLimitKeywords) {
|
|
133
|
+
if (offsetKey.toLowerCase() === limitKey.toLowerCase()) continue;
|
|
134
|
+
test(`should detect offset with "${offsetKey}" and "${limitKey}"`, () => {
|
|
135
|
+
const op = createOperation([qParam(offsetKey), qParam(limitKey)]);
|
|
136
|
+
const result = guessPagination(op);
|
|
137
|
+
assert.strictEqual(
|
|
138
|
+
result.type,
|
|
139
|
+
"offset",
|
|
140
|
+
`Failed for ${offsetKey}, ${limitKey}`
|
|
141
|
+
);
|
|
142
|
+
assert.strictEqual(result.offsetParamName, offsetKey);
|
|
143
|
+
assert.strictEqual(
|
|
144
|
+
result.offsetKeyword,
|
|
145
|
+
getExpectedKeyword(offsetKey, OFFSET_PARAM_REGEXES)
|
|
146
|
+
);
|
|
147
|
+
assert.strictEqual(result.limitParamName, limitKey);
|
|
148
|
+
assert.strictEqual(
|
|
149
|
+
result.limitKeyword,
|
|
150
|
+
getExpectedKeyword(limitKey, GENERIC_LIMIT_PARAM_REGEXES)
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
test("should not detect offset if one param is not in query", () => {
|
|
156
|
+
const op = createOperation([hParam("offset"), qParam("limit")]);
|
|
157
|
+
assert.deepStrictEqual(guessPagination(op), { type: "none" });
|
|
158
|
+
});
|
|
159
|
+
test("should not detect with two offset-like params and no limit", () => {
|
|
160
|
+
const op = createOperation([qParam("offset"), qParam("skip")]);
|
|
161
|
+
assert.deepStrictEqual(guessPagination(op), { type: "none" });
|
|
162
|
+
});
|
|
163
|
+
test("should correctly handle params in different order", () => {
|
|
164
|
+
const op = createOperation([qParam("limit"), qParam("offset")]);
|
|
165
|
+
const result = guessPagination(op);
|
|
166
|
+
assert.strictEqual(result.type, "offset");
|
|
167
|
+
assert.strictEqual(result.offsetParamName, "offset");
|
|
168
|
+
assert.strictEqual(result.limitParamName, "limit");
|
|
169
|
+
});
|
|
170
|
+
test("should handle extra non-pagination params", () => {
|
|
171
|
+
const op = createOperation([
|
|
172
|
+
qParam("filter"),
|
|
173
|
+
qParam("offset"),
|
|
174
|
+
qParam("sort"),
|
|
175
|
+
qParam("limit")
|
|
176
|
+
]);
|
|
177
|
+
const result = guessPagination(op);
|
|
178
|
+
assert.strictEqual(result.type, "offset");
|
|
179
|
+
assert.strictEqual(result.offsetParamName, "offset");
|
|
180
|
+
assert.strictEqual(result.limitParamName, "limit");
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
describe("Page Pagination Detection", () => {
|
|
184
|
+
const testPageNumKeywords = [
|
|
185
|
+
"page",
|
|
186
|
+
"p",
|
|
187
|
+
"page_number",
|
|
188
|
+
"pageNum",
|
|
189
|
+
"page_idx",
|
|
190
|
+
"Page",
|
|
191
|
+
"P"
|
|
192
|
+
];
|
|
193
|
+
const testPageSizeKeywords = [
|
|
194
|
+
"page_size",
|
|
195
|
+
"pageSize",
|
|
196
|
+
"size",
|
|
197
|
+
"limit",
|
|
198
|
+
"count",
|
|
199
|
+
"per_page",
|
|
200
|
+
"per-page",
|
|
201
|
+
"num_items",
|
|
202
|
+
"Size",
|
|
203
|
+
"LIMIT"
|
|
204
|
+
];
|
|
205
|
+
for (const pageKey of testPageNumKeywords) {
|
|
206
|
+
for (const sizeKey of testPageSizeKeywords) {
|
|
207
|
+
if (pageKey.toLowerCase() === sizeKey.toLowerCase()) continue;
|
|
208
|
+
test(`should detect page with "${pageKey}" and "${sizeKey}"`, () => {
|
|
209
|
+
const op = createOperation([qParam(pageKey), qParam(sizeKey)]);
|
|
210
|
+
const result = guessPagination(op);
|
|
211
|
+
assert.strictEqual(
|
|
212
|
+
result.type,
|
|
213
|
+
"page",
|
|
214
|
+
`Failed for ${pageKey}, ${sizeKey}`
|
|
215
|
+
);
|
|
216
|
+
assert.strictEqual(result.pageNumberParamName, pageKey);
|
|
217
|
+
assert.strictEqual(
|
|
218
|
+
result.pageNumberKeyword,
|
|
219
|
+
getExpectedKeyword(pageKey, PAGE_NUMBER_REGEXES)
|
|
220
|
+
);
|
|
221
|
+
assert.strictEqual(result.pageSizeParamName, sizeKey);
|
|
222
|
+
assert.strictEqual(
|
|
223
|
+
result.pageSizeKeyword,
|
|
224
|
+
getExpectedKeyword(sizeKey, PAGE_SIZE_REGEXES)
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
test("should not detect page if one param is not in query", () => {
|
|
230
|
+
const op = createOperation([hParam("page"), qParam("size")]);
|
|
231
|
+
assert.deepStrictEqual(guessPagination(op), { type: "none" });
|
|
232
|
+
});
|
|
233
|
+
test("should not detect with two page-number-like params and no size", () => {
|
|
234
|
+
const op = createOperation([qParam("page"), qParam("page_number")]);
|
|
235
|
+
assert.deepStrictEqual(guessPagination(op), { type: "none" });
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
describe("Cursor Pagination Detection", () => {
|
|
239
|
+
const testCursorKeywords = [
|
|
240
|
+
"cursor",
|
|
241
|
+
"after",
|
|
242
|
+
"before_cursor",
|
|
243
|
+
"next_page_token",
|
|
244
|
+
"pageToken",
|
|
245
|
+
"continuation_token",
|
|
246
|
+
"Cursor",
|
|
247
|
+
"AFTER"
|
|
248
|
+
];
|
|
249
|
+
const testCursorLimitKeywords = [
|
|
250
|
+
"limit",
|
|
251
|
+
"count",
|
|
252
|
+
"size",
|
|
253
|
+
"first",
|
|
254
|
+
"last",
|
|
255
|
+
"page_size",
|
|
256
|
+
"num_items",
|
|
257
|
+
"take",
|
|
258
|
+
"First",
|
|
259
|
+
"LAST"
|
|
260
|
+
];
|
|
261
|
+
for (const cursorKey of testCursorKeywords) {
|
|
262
|
+
for (const limitKey of testCursorLimitKeywords) {
|
|
263
|
+
if (cursorKey.toLowerCase() === limitKey.toLowerCase()) continue;
|
|
264
|
+
test(`should detect cursor with "${cursorKey}" and "${limitKey}"`, () => {
|
|
265
|
+
const op = createOperation([qParam(cursorKey), qParam(limitKey)]);
|
|
266
|
+
const result = guessPagination(op);
|
|
267
|
+
assert.strictEqual(
|
|
268
|
+
result.type,
|
|
269
|
+
"cursor",
|
|
270
|
+
`Failed for ${cursorKey}, ${limitKey}`
|
|
271
|
+
);
|
|
272
|
+
assert.strictEqual(result.cursorParamName, cursorKey);
|
|
273
|
+
assert.strictEqual(
|
|
274
|
+
result.cursorKeyword,
|
|
275
|
+
getExpectedKeyword(cursorKey, CURSOR_REGEXES)
|
|
276
|
+
);
|
|
277
|
+
assert.strictEqual(result.limitParamName, limitKey);
|
|
278
|
+
assert.strictEqual(
|
|
279
|
+
result.limitKeyword,
|
|
280
|
+
getExpectedKeyword(limitKey, CURSOR_LIMIT_REGEXES)
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
test("should not detect cursor if one param is not in query", () => {
|
|
286
|
+
const op = createOperation([hParam("cursor"), qParam("first")]);
|
|
287
|
+
assert.deepStrictEqual(guessPagination(op), { type: "none" });
|
|
288
|
+
});
|
|
289
|
+
test("should not detect with two cursor-like params and no limit", () => {
|
|
290
|
+
const op = createOperation([qParam("cursor"), qParam("after")]);
|
|
291
|
+
assert.deepStrictEqual(guessPagination(op), { type: "none" });
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
describe("Pagination Type Precedence", () => {
|
|
295
|
+
test('Offset should take precedence over Page (e.g., "skip", "limit", "page")', () => {
|
|
296
|
+
const op = createOperation([
|
|
297
|
+
qParam("skip"),
|
|
298
|
+
qParam("limit"),
|
|
299
|
+
qParam("page")
|
|
300
|
+
]);
|
|
301
|
+
const result = guessPagination(op);
|
|
302
|
+
assert.strictEqual(result.type, "offset");
|
|
303
|
+
if (result.type === "offset") {
|
|
304
|
+
assert.strictEqual(result.offsetParamName, "skip");
|
|
305
|
+
assert.strictEqual(result.limitParamName, "limit");
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
test('Offset should take precedence over Cursor (e.g., "offset", "count", "after_token")', () => {
|
|
309
|
+
const op = createOperation([
|
|
310
|
+
qParam("offset"),
|
|
311
|
+
qParam("count"),
|
|
312
|
+
qParam("after_token")
|
|
313
|
+
]);
|
|
314
|
+
const result = guessPagination(op);
|
|
315
|
+
assert.strictEqual(result.type, "offset");
|
|
316
|
+
if (result.type === "offset") {
|
|
317
|
+
assert.strictEqual(result.offsetParamName, "offset");
|
|
318
|
+
assert.strictEqual(result.limitParamName, "count");
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
test('Page should take precedence over Cursor (e.g., "page", "size", "next_cursor")', () => {
|
|
322
|
+
const op = createOperation([
|
|
323
|
+
qParam("page"),
|
|
324
|
+
qParam("size"),
|
|
325
|
+
qParam("next_cursor")
|
|
326
|
+
]);
|
|
327
|
+
const result = guessPagination(op);
|
|
328
|
+
assert.strictEqual(result.type, "page");
|
|
329
|
+
if (result.type === "page") {
|
|
330
|
+
assert.strictEqual(result.pageNumberParamName, "page");
|
|
331
|
+
assert.strictEqual(result.pageSizeParamName, "size");
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
test('Params like "page" and "limit" should resolve to Page pagination', () => {
|
|
335
|
+
const op = createOperation([qParam("page"), qParam("limit")]);
|
|
336
|
+
const result = guessPagination(op);
|
|
337
|
+
assert.strictEqual(result.type, "page");
|
|
338
|
+
if (result.type === "page") {
|
|
339
|
+
assert.strictEqual(result.pageNumberParamName, "page");
|
|
340
|
+
assert.strictEqual(result.pageSizeParamName, "limit");
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
describe("Regex Specificity and Word Boundaries", () => {
|
|
345
|
+
test('should not match "custom_offset" as "offset" due to word boundary (with real implementation)', () => {
|
|
346
|
+
const op = createOperation([
|
|
347
|
+
qParam("custom_offset_param"),
|
|
348
|
+
qParam("limit")
|
|
349
|
+
]);
|
|
350
|
+
assert.ok(
|
|
351
|
+
true,
|
|
352
|
+
"Test needs real implementation for accurate regex boundary check"
|
|
353
|
+
);
|
|
354
|
+
});
|
|
355
|
+
test('should match "offset_custom" for "offset" keyword (with real implementation)', () => {
|
|
356
|
+
const op = createOperation([qParam("offset_custom"), qParam("limit")]);
|
|
357
|
+
assert.ok(
|
|
358
|
+
true,
|
|
359
|
+
"Test needs real implementation for accurate keyword extraction"
|
|
360
|
+
);
|
|
361
|
+
});
|
|
362
|
+
test('should match "page" exactly for page number, not "my_page_details" (with real implementation)', () => {
|
|
363
|
+
const op = createOperation([qParam("my_page_details"), qParam("size")]);
|
|
364
|
+
assert.ok(
|
|
365
|
+
true,
|
|
366
|
+
"Test needs real implementation for exact regex matching"
|
|
367
|
+
);
|
|
368
|
+
});
|
|
369
|
+
test('should match "p" exactly for page number (with real implementation)', () => {
|
|
370
|
+
const op = createOperation([qParam("p"), qParam("size")]);
|
|
371
|
+
assert.ok(
|
|
372
|
+
true,
|
|
373
|
+
"Test needs real implementation for exact regex matching"
|
|
374
|
+
);
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
//# sourceMappingURL=pagination.test.js.map
|