@open-loyalty/mcp-server 1.0.0 → 1.0.2
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/README.md +134 -12
- package/dist/auth/provider.d.ts +33 -0
- package/dist/auth/provider.js +395 -0
- package/dist/auth/storage.d.ts +16 -0
- package/dist/auth/storage.js +98 -0
- package/dist/config.d.ts +12 -0
- package/dist/config.js +22 -0
- package/dist/http.d.ts +2 -0
- package/dist/http.js +214 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -0
- package/package.json +11 -10
- package/dist/tools/member.test.d.ts +0 -1
- package/dist/tools/member.test.js +0 -213
- package/dist/tools/points.test.d.ts +0 -1
- package/dist/tools/points.test.js +0 -292
- package/dist/tools/reward.test.d.ts +0 -1
- package/dist/tools/reward.test.js +0 -240
- package/dist/tools/transaction.test.d.ts +0 -1
- package/dist/tools/transaction.test.js +0 -235
- package/dist/utils/cursor.d.ts +0 -84
- package/dist/utils/cursor.js +0 -117
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
// src/tools/transaction.test.ts
|
|
2
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
3
|
-
import { setupMockAxios, teardownMockAxios, getMockAxios } from '../../tests/mocks/http.mock.js';
|
|
4
|
-
import { transactionFixtures } from '../../tests/fixtures/transaction.fixtures.js';
|
|
5
|
-
import { transactionCreate, transactionGet, transactionList, transactionAssignMember, } from './transaction.js';
|
|
6
|
-
describe('Transaction Operations', () => {
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
setupMockAxios();
|
|
9
|
-
});
|
|
10
|
-
afterEach(() => {
|
|
11
|
-
teardownMockAxios();
|
|
12
|
-
});
|
|
13
|
-
describe('transactionCreate', () => {
|
|
14
|
-
it('should create transaction with transaction wrapper', async () => {
|
|
15
|
-
const mockAxios = getMockAxios();
|
|
16
|
-
mockAxios.onPost('/default/transaction').reply(200, transactionFixtures.createResponse);
|
|
17
|
-
const result = await transactionCreate({
|
|
18
|
-
header: {
|
|
19
|
-
documentNumber: 'DOC-001',
|
|
20
|
-
purchasedAt: '2026-01-15T10:00:00Z',
|
|
21
|
-
},
|
|
22
|
-
items: [
|
|
23
|
-
{
|
|
24
|
-
sku: 'PROD-001',
|
|
25
|
-
name: 'Test Product',
|
|
26
|
-
grossValue: 100,
|
|
27
|
-
category: 'Electronics',
|
|
28
|
-
},
|
|
29
|
-
],
|
|
30
|
-
});
|
|
31
|
-
expect(result.transactionId).toBe('txn-uuid-123');
|
|
32
|
-
// Verify request body format with transaction wrapper
|
|
33
|
-
const requestData = JSON.parse(mockAxios.history.post[0].data);
|
|
34
|
-
expect(requestData.transaction).toBeDefined();
|
|
35
|
-
expect(requestData.transaction.header.documentNumber).toBe('DOC-001');
|
|
36
|
-
expect(requestData.transaction.header.documentType).toBe('sell');
|
|
37
|
-
expect(requestData.transaction.items).toHaveLength(1);
|
|
38
|
-
expect(requestData.transaction.items[0].sku).toBe('PROD-001');
|
|
39
|
-
});
|
|
40
|
-
it('should include customerData for member matching', async () => {
|
|
41
|
-
const mockAxios = getMockAxios();
|
|
42
|
-
mockAxios.onPost('/default/transaction').reply(200, {
|
|
43
|
-
transactionId: 'txn-uuid',
|
|
44
|
-
pointsEarned: 100,
|
|
45
|
-
});
|
|
46
|
-
const result = await transactionCreate({
|
|
47
|
-
header: {
|
|
48
|
-
documentNumber: 'DOC-002',
|
|
49
|
-
purchasedAt: '2026-01-15T10:00:00Z',
|
|
50
|
-
},
|
|
51
|
-
items: [
|
|
52
|
-
{
|
|
53
|
-
sku: 'PROD-001',
|
|
54
|
-
name: 'Product',
|
|
55
|
-
grossValue: 100,
|
|
56
|
-
category: 'Category',
|
|
57
|
-
},
|
|
58
|
-
],
|
|
59
|
-
customerData: {
|
|
60
|
-
email: 'customer@example.com',
|
|
61
|
-
},
|
|
62
|
-
});
|
|
63
|
-
expect(result.pointsEarned).toBe(100);
|
|
64
|
-
const requestData = JSON.parse(mockAxios.history.post[0].data);
|
|
65
|
-
expect(requestData.transaction.customerData.email).toBe('customer@example.com');
|
|
66
|
-
});
|
|
67
|
-
it('should handle return transactions', async () => {
|
|
68
|
-
const mockAxios = getMockAxios();
|
|
69
|
-
mockAxios.onPost('/default/transaction').reply(200, transactionFixtures.createResponse);
|
|
70
|
-
await transactionCreate({
|
|
71
|
-
header: {
|
|
72
|
-
documentNumber: 'RETURN-001',
|
|
73
|
-
purchasedAt: '2026-01-15T10:00:00Z',
|
|
74
|
-
documentType: 'return',
|
|
75
|
-
linkedDocumentNumber: 'DOC-001',
|
|
76
|
-
},
|
|
77
|
-
items: [
|
|
78
|
-
{
|
|
79
|
-
sku: 'PROD-001',
|
|
80
|
-
name: 'Product',
|
|
81
|
-
grossValue: -50,
|
|
82
|
-
category: 'Category',
|
|
83
|
-
},
|
|
84
|
-
],
|
|
85
|
-
});
|
|
86
|
-
const requestData = JSON.parse(mockAxios.history.post[0].data);
|
|
87
|
-
expect(requestData.transaction.header.documentType).toBe('return');
|
|
88
|
-
expect(requestData.transaction.header.linkedDocumentNumber).toBe('DOC-001');
|
|
89
|
-
});
|
|
90
|
-
it('should use highPrecisionQuantity', async () => {
|
|
91
|
-
const mockAxios = getMockAxios();
|
|
92
|
-
mockAxios.onPost('/default/transaction').reply(200, transactionFixtures.createResponse);
|
|
93
|
-
await transactionCreate({
|
|
94
|
-
header: {
|
|
95
|
-
documentNumber: 'DOC-003',
|
|
96
|
-
purchasedAt: '2026-01-15T10:00:00Z',
|
|
97
|
-
},
|
|
98
|
-
items: [
|
|
99
|
-
{
|
|
100
|
-
sku: 'PROD-001',
|
|
101
|
-
name: 'Product',
|
|
102
|
-
grossValue: 100,
|
|
103
|
-
category: 'Category',
|
|
104
|
-
highPrecisionQuantity: 1.5,
|
|
105
|
-
},
|
|
106
|
-
],
|
|
107
|
-
});
|
|
108
|
-
const requestData = JSON.parse(mockAxios.history.post[0].data);
|
|
109
|
-
expect(requestData.transaction.items[0].highPrecisionQuantity).toBe(1.5);
|
|
110
|
-
});
|
|
111
|
-
it('should use custom storeCode', async () => {
|
|
112
|
-
const mockAxios = getMockAxios();
|
|
113
|
-
mockAxios.onPost('/custom-store/transaction').reply(200, transactionFixtures.createResponse);
|
|
114
|
-
await transactionCreate({
|
|
115
|
-
storeCode: 'custom-store',
|
|
116
|
-
header: {
|
|
117
|
-
documentNumber: 'DOC-001',
|
|
118
|
-
purchasedAt: '2026-01-15T10:00:00Z',
|
|
119
|
-
},
|
|
120
|
-
items: [
|
|
121
|
-
{
|
|
122
|
-
sku: 'PROD-001',
|
|
123
|
-
name: 'Product',
|
|
124
|
-
grossValue: 100,
|
|
125
|
-
category: 'Category',
|
|
126
|
-
},
|
|
127
|
-
],
|
|
128
|
-
});
|
|
129
|
-
expect(mockAxios.history.post[0].url).toBe('/custom-store/transaction');
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
describe('transactionGet', () => {
|
|
133
|
-
it('should get transaction details', async () => {
|
|
134
|
-
const mockAxios = getMockAxios();
|
|
135
|
-
mockAxios.onGet('/default/transaction/txn-uuid').reply(200, transactionFixtures.getResponse);
|
|
136
|
-
const result = await transactionGet({ transactionId: 'txn-uuid' });
|
|
137
|
-
expect(result.transactionId).toBe('txn-uuid-123');
|
|
138
|
-
expect(result.documentNumber).toBe('DOC-001');
|
|
139
|
-
expect(result.documentType).toBe('sell');
|
|
140
|
-
expect(result.matched).toBe(true);
|
|
141
|
-
expect(result.grossValue).toBe(150);
|
|
142
|
-
expect(result.pointsEarned).toBe(150);
|
|
143
|
-
});
|
|
144
|
-
it('should extract documentNumber from header', async () => {
|
|
145
|
-
const mockAxios = getMockAxios();
|
|
146
|
-
mockAxios.onGet('/default/transaction/txn-uuid').reply(200, {
|
|
147
|
-
transactionId: 'txn-uuid',
|
|
148
|
-
header: {
|
|
149
|
-
documentNumber: 'DOC-FROM-HEADER',
|
|
150
|
-
documentType: 'sell',
|
|
151
|
-
purchasedAt: '2026-01-15T10:00:00Z',
|
|
152
|
-
},
|
|
153
|
-
items: [],
|
|
154
|
-
matched: true,
|
|
155
|
-
grossValue: 100,
|
|
156
|
-
});
|
|
157
|
-
const result = await transactionGet({ transactionId: 'txn-uuid' });
|
|
158
|
-
expect(result.documentNumber).toBe('DOC-FROM-HEADER');
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
describe('transactionList', () => {
|
|
162
|
-
it('should list transactions from items array', async () => {
|
|
163
|
-
const mockAxios = getMockAxios();
|
|
164
|
-
mockAxios.onGet('/default/transaction').reply(200, transactionFixtures.listResponse);
|
|
165
|
-
const result = await transactionList({});
|
|
166
|
-
expect(result.transactions).toHaveLength(2);
|
|
167
|
-
expect(result.transactions[0].transactionId).toBe('txn-uuid-1');
|
|
168
|
-
expect(result.transactions[0].documentNumber).toBe('DOC-001');
|
|
169
|
-
expect(result.transactions[0].matched).toBe(true);
|
|
170
|
-
});
|
|
171
|
-
it('should include pagination params', async () => {
|
|
172
|
-
const mockAxios = getMockAxios();
|
|
173
|
-
mockAxios.onGet(/\/default\/transaction/).reply(200, transactionFixtures.listResponse);
|
|
174
|
-
await transactionList({ page: 2, perPage: 25 });
|
|
175
|
-
const url = mockAxios.history.get[0].url;
|
|
176
|
-
expect(url).toContain('_page=2');
|
|
177
|
-
expect(url).toContain('_itemsOnPage=25');
|
|
178
|
-
});
|
|
179
|
-
it('should include filter params', async () => {
|
|
180
|
-
const mockAxios = getMockAxios();
|
|
181
|
-
mockAxios.onGet(/\/default\/transaction/).reply(200, transactionFixtures.listResponse);
|
|
182
|
-
await transactionList({
|
|
183
|
-
customerId: 'member-uuid',
|
|
184
|
-
documentNumber: 'DOC-001',
|
|
185
|
-
documentType: 'sell',
|
|
186
|
-
matched: true,
|
|
187
|
-
});
|
|
188
|
-
const url = mockAxios.history.get[0].url;
|
|
189
|
-
expect(url).toContain('customerId=member-uuid');
|
|
190
|
-
expect(url).toContain('header%3AdocumentNumber=DOC-001');
|
|
191
|
-
expect(url).toContain('header%3AdocumentType=sell');
|
|
192
|
-
expect(url).toContain('matched=true');
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
describe('transactionAssignMember', () => {
|
|
196
|
-
it('should assign transaction to member by customerId', async () => {
|
|
197
|
-
const mockAxios = getMockAxios();
|
|
198
|
-
mockAxios.onPost('/default/transaction/customer/assign').reply(200, transactionFixtures.assignResponse);
|
|
199
|
-
const result = await transactionAssignMember({
|
|
200
|
-
documentNumber: 'DOC-001',
|
|
201
|
-
customerId: 'member-uuid',
|
|
202
|
-
});
|
|
203
|
-
expect(result.transactionId).toBe('txn-uuid-123');
|
|
204
|
-
expect(result.customerId).toBe('member-uuid');
|
|
205
|
-
const requestData = JSON.parse(mockAxios.history.post[0].data);
|
|
206
|
-
expect(requestData.transactionDocumentNumber).toBe('DOC-001');
|
|
207
|
-
expect(requestData.customerId).toBe('member-uuid');
|
|
208
|
-
});
|
|
209
|
-
it('should assign by loyalty card number', async () => {
|
|
210
|
-
const mockAxios = getMockAxios();
|
|
211
|
-
mockAxios.onPost('/default/transaction/customer/assign').reply(200, transactionFixtures.assignResponse);
|
|
212
|
-
await transactionAssignMember({
|
|
213
|
-
documentNumber: 'DOC-001',
|
|
214
|
-
loyaltyCardNumber: 'CARD123456',
|
|
215
|
-
});
|
|
216
|
-
const requestData = JSON.parse(mockAxios.history.post[0].data);
|
|
217
|
-
expect(requestData.customerLoyaltyCardNumber).toBe('CARD123456');
|
|
218
|
-
});
|
|
219
|
-
it('should assign by phone number', async () => {
|
|
220
|
-
const mockAxios = getMockAxios();
|
|
221
|
-
mockAxios.onPost('/default/transaction/customer/assign').reply(200, transactionFixtures.assignResponse);
|
|
222
|
-
await transactionAssignMember({
|
|
223
|
-
documentNumber: 'DOC-001',
|
|
224
|
-
phone: '+1234567890',
|
|
225
|
-
});
|
|
226
|
-
const requestData = JSON.parse(mockAxios.history.post[0].data);
|
|
227
|
-
expect(requestData.customerPhoneNumber).toBe('+1234567890');
|
|
228
|
-
});
|
|
229
|
-
it('should throw error when no identifier provided', async () => {
|
|
230
|
-
await expect(transactionAssignMember({
|
|
231
|
-
documentNumber: 'DOC-001',
|
|
232
|
-
})).rejects.toThrow('At least one member identifier required');
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
});
|
package/dist/utils/cursor.d.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cursor-based pagination utilities for Open Loyalty MCP.
|
|
3
|
-
*
|
|
4
|
-
* Since the Open Loyalty API only supports offset-based pagination (_page/_itemsOnPage),
|
|
5
|
-
* we implement client-side cursor emulation by encoding pagination state into opaque cursors.
|
|
6
|
-
*/
|
|
7
|
-
/**
|
|
8
|
-
* Internal cursor data structure.
|
|
9
|
-
* Encoded as base64url JSON for the external cursor string.
|
|
10
|
-
*/
|
|
11
|
-
export interface CursorData {
|
|
12
|
-
/** Page number (1-indexed) */
|
|
13
|
-
p: number;
|
|
14
|
-
/** Items per page */
|
|
15
|
-
pp: number;
|
|
16
|
-
/** Version for future migrations */
|
|
17
|
-
v: number;
|
|
18
|
-
/** Optional timestamp for expiration (not currently enforced) */
|
|
19
|
-
ts?: number;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Pagination metadata returned in list responses.
|
|
23
|
-
*/
|
|
24
|
-
export interface PaginationMeta {
|
|
25
|
-
/** Current cursor (encodes current page state) */
|
|
26
|
-
cursor: string;
|
|
27
|
-
/** Cursor for the next page, or undefined if no more pages */
|
|
28
|
-
nextCursor: string | undefined;
|
|
29
|
-
/** Whether there are more items after the current page */
|
|
30
|
-
hasMore: boolean;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Encode pagination state into an opaque cursor string.
|
|
34
|
-
* Uses base64url encoding of JSON data.
|
|
35
|
-
*
|
|
36
|
-
* @param data - Pagination state to encode
|
|
37
|
-
* @returns Opaque cursor string
|
|
38
|
-
*/
|
|
39
|
-
export declare function encodeCursor(data: Omit<CursorData, 'v'>): string;
|
|
40
|
-
/**
|
|
41
|
-
* Decode an opaque cursor string back to pagination state.
|
|
42
|
-
*
|
|
43
|
-
* @param cursor - Opaque cursor string
|
|
44
|
-
* @returns Decoded pagination state, or null if invalid
|
|
45
|
-
*/
|
|
46
|
-
export declare function decodeCursor(cursor: string): CursorData | null;
|
|
47
|
-
/**
|
|
48
|
-
* Build pagination metadata for a list response.
|
|
49
|
-
*
|
|
50
|
-
* @param page - Current page number (1-indexed)
|
|
51
|
-
* @param perPage - Items per page
|
|
52
|
-
* @param itemCount - Number of items returned in current page
|
|
53
|
-
* @param total - Total number of items (from API response, may be undefined)
|
|
54
|
-
* @returns Pagination metadata with cursors
|
|
55
|
-
*/
|
|
56
|
-
export declare function buildPaginationMeta(page: number, perPage: number, itemCount: number, total?: number): PaginationMeta;
|
|
57
|
-
/**
|
|
58
|
-
* Pagination input that supports both cursor and page/perPage.
|
|
59
|
-
*/
|
|
60
|
-
export interface PaginationInput {
|
|
61
|
-
/** Opaque cursor from previous response (takes priority over page/perPage) */
|
|
62
|
-
cursor?: string;
|
|
63
|
-
/** Page number (1-indexed, used if no cursor) */
|
|
64
|
-
page?: number;
|
|
65
|
-
/** Items per page (used if no cursor) */
|
|
66
|
-
perPage?: number;
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Resolved pagination parameters for API calls.
|
|
70
|
-
*/
|
|
71
|
-
export interface ResolvedPagination {
|
|
72
|
-
/** Page number to request (1-indexed) */
|
|
73
|
-
page: number;
|
|
74
|
-
/** Items per page */
|
|
75
|
-
perPage: number;
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Resolve pagination input to concrete page/perPage values.
|
|
79
|
-
* If a cursor is provided and valid, it takes priority over page/perPage.
|
|
80
|
-
*
|
|
81
|
-
* @param input - Pagination input with optional cursor
|
|
82
|
-
* @returns Resolved page and perPage values
|
|
83
|
-
*/
|
|
84
|
-
export declare function resolvePaginationInput(input: PaginationInput): ResolvedPagination;
|
package/dist/utils/cursor.js
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cursor-based pagination utilities for Open Loyalty MCP.
|
|
3
|
-
*
|
|
4
|
-
* Since the Open Loyalty API only supports offset-based pagination (_page/_itemsOnPage),
|
|
5
|
-
* we implement client-side cursor emulation by encoding pagination state into opaque cursors.
|
|
6
|
-
*/
|
|
7
|
-
/** Current cursor format version */
|
|
8
|
-
const CURSOR_VERSION = 1;
|
|
9
|
-
/**
|
|
10
|
-
* Encode pagination state into an opaque cursor string.
|
|
11
|
-
* Uses base64url encoding of JSON data.
|
|
12
|
-
*
|
|
13
|
-
* @param data - Pagination state to encode
|
|
14
|
-
* @returns Opaque cursor string
|
|
15
|
-
*/
|
|
16
|
-
export function encodeCursor(data) {
|
|
17
|
-
const cursorData = {
|
|
18
|
-
...data,
|
|
19
|
-
v: CURSOR_VERSION,
|
|
20
|
-
};
|
|
21
|
-
const json = JSON.stringify(cursorData);
|
|
22
|
-
// Use base64url encoding (URL-safe base64)
|
|
23
|
-
return Buffer.from(json, 'utf-8')
|
|
24
|
-
.toString('base64')
|
|
25
|
-
.replace(/\+/g, '-')
|
|
26
|
-
.replace(/\//g, '_')
|
|
27
|
-
.replace(/=+$/, '');
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Decode an opaque cursor string back to pagination state.
|
|
31
|
-
*
|
|
32
|
-
* @param cursor - Opaque cursor string
|
|
33
|
-
* @returns Decoded pagination state, or null if invalid
|
|
34
|
-
*/
|
|
35
|
-
export function decodeCursor(cursor) {
|
|
36
|
-
try {
|
|
37
|
-
// Restore base64 from base64url
|
|
38
|
-
let base64 = cursor.replace(/-/g, '+').replace(/_/g, '/');
|
|
39
|
-
// Add padding if needed
|
|
40
|
-
const paddingNeeded = (4 - (base64.length % 4)) % 4;
|
|
41
|
-
base64 += '='.repeat(paddingNeeded);
|
|
42
|
-
const json = Buffer.from(base64, 'base64').toString('utf-8');
|
|
43
|
-
const data = JSON.parse(json);
|
|
44
|
-
// Validate required fields
|
|
45
|
-
if (typeof data.p !== 'number' || data.p < 1)
|
|
46
|
-
return null;
|
|
47
|
-
if (typeof data.pp !== 'number' || data.pp < 1)
|
|
48
|
-
return null;
|
|
49
|
-
if (typeof data.v !== 'number')
|
|
50
|
-
return null;
|
|
51
|
-
return data;
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Build pagination metadata for a list response.
|
|
59
|
-
*
|
|
60
|
-
* @param page - Current page number (1-indexed)
|
|
61
|
-
* @param perPage - Items per page
|
|
62
|
-
* @param itemCount - Number of items returned in current page
|
|
63
|
-
* @param total - Total number of items (from API response, may be undefined)
|
|
64
|
-
* @returns Pagination metadata with cursors
|
|
65
|
-
*/
|
|
66
|
-
export function buildPaginationMeta(page, perPage, itemCount, total) {
|
|
67
|
-
// Build current cursor
|
|
68
|
-
const cursor = encodeCursor({ p: page, pp: perPage });
|
|
69
|
-
// Determine if there are more pages
|
|
70
|
-
let hasMore = false;
|
|
71
|
-
if (total !== undefined && typeof total === 'number') {
|
|
72
|
-
// If we have total, calculate if there are more pages
|
|
73
|
-
hasMore = page * perPage < total;
|
|
74
|
-
}
|
|
75
|
-
else {
|
|
76
|
-
// If no total, assume more pages if we got a full page of results
|
|
77
|
-
hasMore = itemCount >= perPage;
|
|
78
|
-
}
|
|
79
|
-
// Build next cursor if there are more pages
|
|
80
|
-
const nextCursor = hasMore
|
|
81
|
-
? encodeCursor({ p: page + 1, pp: perPage })
|
|
82
|
-
: undefined;
|
|
83
|
-
return {
|
|
84
|
-
cursor,
|
|
85
|
-
nextCursor,
|
|
86
|
-
hasMore,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
/** Default page number if not specified */
|
|
90
|
-
const DEFAULT_PAGE = 1;
|
|
91
|
-
/** Default items per page if not specified */
|
|
92
|
-
const DEFAULT_PER_PAGE = 25;
|
|
93
|
-
/**
|
|
94
|
-
* Resolve pagination input to concrete page/perPage values.
|
|
95
|
-
* If a cursor is provided and valid, it takes priority over page/perPage.
|
|
96
|
-
*
|
|
97
|
-
* @param input - Pagination input with optional cursor
|
|
98
|
-
* @returns Resolved page and perPage values
|
|
99
|
-
*/
|
|
100
|
-
export function resolvePaginationInput(input) {
|
|
101
|
-
// If cursor is provided, try to decode it
|
|
102
|
-
if (input.cursor) {
|
|
103
|
-
const cursorData = decodeCursor(input.cursor);
|
|
104
|
-
if (cursorData) {
|
|
105
|
-
return {
|
|
106
|
-
page: cursorData.p,
|
|
107
|
-
perPage: cursorData.pp,
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
// Invalid cursor - fall through to page/perPage
|
|
111
|
-
}
|
|
112
|
-
// Use page/perPage with defaults
|
|
113
|
-
return {
|
|
114
|
-
page: input.page ?? DEFAULT_PAGE,
|
|
115
|
-
perPage: input.perPage ?? DEFAULT_PER_PAGE,
|
|
116
|
-
};
|
|
117
|
-
}
|