@objectql/server 1.7.1 → 1.7.3
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/CHANGELOG.md +18 -0
- package/LICENSE +118 -21
- package/README.md +93 -0
- package/dist/adapters/rest.js +1 -1
- package/dist/adapters/rest.js.map +1 -1
- package/dist/metadata.js +7 -7
- package/dist/metadata.js.map +1 -1
- package/dist/server.d.ts +4 -0
- package/dist/server.js +52 -10
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +14 -7
- package/package.json +4 -3
- package/src/adapters/rest.ts +1 -1
- package/src/metadata.ts +7 -7
- package/src/server.ts +58 -11
- package/src/types.ts +23 -7
- package/test/integration-example.ts +247 -0
- package/test/node.test.ts +4 -2
- package/test/rest.test.ts +47 -7
- package/tsconfig.tsbuildinfo +1 -1
package/src/server.ts
CHANGED
|
@@ -58,17 +58,27 @@ export class ObjectQLServer {
|
|
|
58
58
|
switch (req.op) {
|
|
59
59
|
case 'find':
|
|
60
60
|
result = await repo.find(req.args);
|
|
61
|
-
|
|
61
|
+
// For find operations, return items array with pagination metadata
|
|
62
|
+
return this.buildListResponse(result, req.args, repo);
|
|
62
63
|
case 'findOne':
|
|
63
64
|
// Support both string ID and query object
|
|
64
65
|
result = await repo.findOne(req.args);
|
|
65
|
-
|
|
66
|
+
if (result) {
|
|
67
|
+
return { ...result, '@type': req.object };
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
66
70
|
case 'create':
|
|
67
71
|
result = await repo.create(req.args);
|
|
68
|
-
|
|
72
|
+
if (result) {
|
|
73
|
+
return { ...result, '@type': req.object };
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
69
76
|
case 'update':
|
|
70
77
|
result = await repo.update(req.args.id, req.args.data);
|
|
71
|
-
|
|
78
|
+
if (result) {
|
|
79
|
+
return { ...result, '@type': req.object };
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
72
82
|
case 'delete':
|
|
73
83
|
result = await repo.delete(req.args.id);
|
|
74
84
|
if (!result) {
|
|
@@ -77,12 +87,15 @@ export class ObjectQLServer {
|
|
|
77
87
|
`Record with id '${req.args.id}' not found for delete`
|
|
78
88
|
);
|
|
79
89
|
}
|
|
80
|
-
// Return standardized delete response
|
|
81
|
-
|
|
82
|
-
|
|
90
|
+
// Return standardized delete response with object type
|
|
91
|
+
return {
|
|
92
|
+
id: req.args.id,
|
|
93
|
+
deleted: true,
|
|
94
|
+
'@type': req.object
|
|
95
|
+
};
|
|
83
96
|
case 'count':
|
|
84
97
|
result = await repo.count(req.args);
|
|
85
|
-
|
|
98
|
+
return { count: result, '@type': req.object };
|
|
86
99
|
case 'action':
|
|
87
100
|
// Map generic args to ActionContext
|
|
88
101
|
result = await app.executeAction(req.object, req.args.action, {
|
|
@@ -90,7 +103,10 @@ export class ObjectQLServer {
|
|
|
90
103
|
id: req.args.id,
|
|
91
104
|
input: req.args.input || req.args.params // Support both for convenience
|
|
92
105
|
});
|
|
93
|
-
|
|
106
|
+
if (result && typeof result === 'object') {
|
|
107
|
+
return { ...result, '@type': req.object };
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
94
110
|
default:
|
|
95
111
|
return this.errorResponse(
|
|
96
112
|
ErrorCode.INVALID_REQUEST,
|
|
@@ -98,13 +114,44 @@ export class ObjectQLServer {
|
|
|
98
114
|
);
|
|
99
115
|
}
|
|
100
116
|
|
|
101
|
-
return { data: result };
|
|
102
|
-
|
|
103
117
|
} catch (e: any) {
|
|
104
118
|
return this.handleError(e);
|
|
105
119
|
}
|
|
106
120
|
}
|
|
107
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Build a standardized list response with pagination metadata
|
|
124
|
+
*/
|
|
125
|
+
private async buildListResponse(items: any[], args: any, repo: any): Promise<ObjectQLResponse> {
|
|
126
|
+
const response: ObjectQLResponse = {
|
|
127
|
+
items
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Calculate pagination metadata if limit/skip are present
|
|
131
|
+
if (args && (args.limit || args.skip)) {
|
|
132
|
+
const skip = args.skip || 0;
|
|
133
|
+
const limit = args.limit || items.length;
|
|
134
|
+
|
|
135
|
+
// Get total count - use the same arguments as the query to ensure consistency
|
|
136
|
+
const total = await repo.count(args || {});
|
|
137
|
+
|
|
138
|
+
const size = limit;
|
|
139
|
+
const page = limit > 0 ? Math.floor(skip / limit) + 1 : 1;
|
|
140
|
+
const pages = limit > 0 ? Math.ceil(total / limit) : 1;
|
|
141
|
+
const has_next = skip + items.length < total;
|
|
142
|
+
|
|
143
|
+
response.meta = {
|
|
144
|
+
total,
|
|
145
|
+
page,
|
|
146
|
+
size,
|
|
147
|
+
pages,
|
|
148
|
+
has_next
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return response;
|
|
153
|
+
}
|
|
154
|
+
|
|
108
155
|
/**
|
|
109
156
|
* Handle errors and convert them to appropriate error responses
|
|
110
157
|
*/
|
package/src/types.ts
CHANGED
|
@@ -60,19 +60,35 @@ export interface ErrorDetails {
|
|
|
60
60
|
[key: string]: unknown;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Pagination metadata
|
|
65
|
+
*/
|
|
66
|
+
export interface PaginationMeta {
|
|
67
|
+
total: number; // Total number of records
|
|
68
|
+
page?: number; // Current page number (1-indexed, e.g. page 1 corresponds to skip=0)
|
|
69
|
+
size?: number; // Number of items per page
|
|
70
|
+
pages?: number; // Total number of pages
|
|
71
|
+
has_next?: boolean; // Whether there is a next page
|
|
72
|
+
}
|
|
73
|
+
|
|
63
74
|
/**
|
|
64
75
|
* ObjectQL API response
|
|
65
76
|
*/
|
|
66
77
|
export interface ObjectQLResponse {
|
|
67
|
-
|
|
78
|
+
// For list operations (find)
|
|
79
|
+
items?: any[];
|
|
80
|
+
|
|
81
|
+
// Pagination metadata (for list operations)
|
|
82
|
+
meta?: PaginationMeta;
|
|
83
|
+
|
|
84
|
+
// Error information
|
|
68
85
|
error?: {
|
|
69
86
|
code: ErrorCode | string;
|
|
70
87
|
message: string;
|
|
71
|
-
details?: ErrorDetails;
|
|
72
|
-
};
|
|
73
|
-
meta?: {
|
|
74
|
-
total?: number;
|
|
75
|
-
page?: number;
|
|
76
|
-
per_page?: number;
|
|
88
|
+
details?: ErrorDetails | any; // Allow flexible details structure
|
|
77
89
|
};
|
|
90
|
+
|
|
91
|
+
// For single item operations, the response is the object itself with '@type' field
|
|
92
|
+
// This allows any additional fields from the actual data object
|
|
93
|
+
[key: string]: any;
|
|
78
94
|
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration Example: Demonstrates the new standardized API response format
|
|
3
|
+
*
|
|
4
|
+
* This file is for documentation purposes and shows how the API responses
|
|
5
|
+
* look with the new standardized format.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ObjectQL } from '@objectql/core';
|
|
9
|
+
import { createRESTHandler } from '../src/adapters/rest';
|
|
10
|
+
import { Driver } from '@objectql/types';
|
|
11
|
+
|
|
12
|
+
// Example: Setting up ObjectQL with a simple in-memory driver
|
|
13
|
+
class InMemoryDriver implements Driver {
|
|
14
|
+
private data: Record<string, any[]> = {};
|
|
15
|
+
|
|
16
|
+
async init() {}
|
|
17
|
+
|
|
18
|
+
async find(objectName: string, query: any) {
|
|
19
|
+
let items = this.data[objectName] || [];
|
|
20
|
+
|
|
21
|
+
// Apply skip and limit for pagination
|
|
22
|
+
if (query) {
|
|
23
|
+
if (query.skip) {
|
|
24
|
+
items = items.slice(query.skip);
|
|
25
|
+
}
|
|
26
|
+
if (query.limit) {
|
|
27
|
+
items = items.slice(0, query.limit);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return items;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async findOne(objectName: string, id: string | number) {
|
|
35
|
+
const items = this.data[objectName] || [];
|
|
36
|
+
return items.find(item => item.id === String(id)) || null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async create(objectName: string, data: any) {
|
|
40
|
+
if (!this.data[objectName]) {
|
|
41
|
+
this.data[objectName] = [];
|
|
42
|
+
}
|
|
43
|
+
const newItem = { ...data, id: String(Date.now()) };
|
|
44
|
+
this.data[objectName].push(newItem);
|
|
45
|
+
return newItem;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async update(objectName: string, id: string | number, data: any) {
|
|
49
|
+
const items = this.data[objectName] || [];
|
|
50
|
+
const index = items.findIndex(item => item.id === String(id));
|
|
51
|
+
if (index >= 0) {
|
|
52
|
+
this.data[objectName][index] = { ...items[index], ...data };
|
|
53
|
+
return this.data[objectName][index];
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async delete(objectName: string, id: string | number) {
|
|
59
|
+
const items = this.data[objectName] || [];
|
|
60
|
+
const index = items.findIndex(item => item.id === String(id));
|
|
61
|
+
if (index >= 0) {
|
|
62
|
+
this.data[objectName].splice(index, 1);
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async count(objectName: string) {
|
|
69
|
+
return (this.data[objectName] || []).length;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Seed some sample data
|
|
73
|
+
seed(objectName: string, items: any[]) {
|
|
74
|
+
this.data[objectName] = items;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Initialize ObjectQL
|
|
79
|
+
const driver = new InMemoryDriver();
|
|
80
|
+
const app = new ObjectQL({
|
|
81
|
+
datasources: {
|
|
82
|
+
default: driver
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Register a simple object schema
|
|
87
|
+
app.metadata.register('object', {
|
|
88
|
+
type: 'object',
|
|
89
|
+
id: 'contract',
|
|
90
|
+
content: {
|
|
91
|
+
name: 'contract',
|
|
92
|
+
label: 'Contract',
|
|
93
|
+
fields: {
|
|
94
|
+
name: { type: 'text', label: 'Contract Name' },
|
|
95
|
+
amount: { type: 'number', label: 'Amount' },
|
|
96
|
+
status: { type: 'text', label: 'Status' }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Seed sample data
|
|
102
|
+
driver.seed('contract', [
|
|
103
|
+
{ id: '1', name: 'Contract A', amount: 5000, status: 'active' },
|
|
104
|
+
{ id: '2', name: 'Contract B', amount: 3000, status: 'pending' },
|
|
105
|
+
{ id: '3', name: 'Contract C', amount: 7500, status: 'active' },
|
|
106
|
+
{ id: '4', name: 'Contract D', amount: 2000, status: 'completed' },
|
|
107
|
+
{ id: '5', name: 'Contract E', amount: 9000, status: 'active' },
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Example 1: List all contracts (no pagination)
|
|
112
|
+
*
|
|
113
|
+
* Request: GET /api/data/contract
|
|
114
|
+
*
|
|
115
|
+
* Response:
|
|
116
|
+
* {
|
|
117
|
+
* "items": [
|
|
118
|
+
* { "id": "1", "name": "Contract A", "amount": 5000, "status": "active" },
|
|
119
|
+
* { "id": "2", "name": "Contract B", "amount": 3000, "status": "pending" },
|
|
120
|
+
* { "id": "3", "name": "Contract C", "amount": 7500, "status": "active" },
|
|
121
|
+
* { "id": "4", "name": "Contract D", "amount": 2000, "status": "completed" },
|
|
122
|
+
* { "id": "5", "name": "Contract E", "amount": 9000, "status": "active" }
|
|
123
|
+
* ]
|
|
124
|
+
* }
|
|
125
|
+
*/
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Example 2: List contracts with pagination (first page)
|
|
129
|
+
*
|
|
130
|
+
* Request: GET /api/data/contract?limit=2&skip=0
|
|
131
|
+
*
|
|
132
|
+
* Response:
|
|
133
|
+
* {
|
|
134
|
+
* "items": [
|
|
135
|
+
* { "id": "1", "name": "Contract A", "amount": 5000, "status": "active" },
|
|
136
|
+
* { "id": "2", "name": "Contract B", "amount": 3000, "status": "pending" }
|
|
137
|
+
* ],
|
|
138
|
+
* "meta": {
|
|
139
|
+
* "total": 5,
|
|
140
|
+
* "page": 1,
|
|
141
|
+
* "size": 2,
|
|
142
|
+
* "pages": 3,
|
|
143
|
+
* "has_next": true
|
|
144
|
+
* }
|
|
145
|
+
* }
|
|
146
|
+
*/
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Example 3: List contracts with pagination (second page)
|
|
150
|
+
*
|
|
151
|
+
* Request: GET /api/data/contract?limit=2&skip=2
|
|
152
|
+
*
|
|
153
|
+
* Response:
|
|
154
|
+
* {
|
|
155
|
+
* "items": [
|
|
156
|
+
* { "id": "3", "name": "Contract C", "amount": 7500, "status": "active" },
|
|
157
|
+
* { "id": "4", "name": "Contract D", "amount": 2000, "status": "completed" }
|
|
158
|
+
* ],
|
|
159
|
+
* "meta": {
|
|
160
|
+
* "total": 5,
|
|
161
|
+
* "page": 2,
|
|
162
|
+
* "size": 2,
|
|
163
|
+
* "pages": 3,
|
|
164
|
+
* "has_next": true
|
|
165
|
+
* }
|
|
166
|
+
* }
|
|
167
|
+
*/
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Example 4: Get a single contract
|
|
171
|
+
*
|
|
172
|
+
* Request: GET /api/data/contract/1
|
|
173
|
+
*
|
|
174
|
+
* Response:
|
|
175
|
+
* {
|
|
176
|
+
* "data": {
|
|
177
|
+
* "id": "1",
|
|
178
|
+
* "name": "Contract A",
|
|
179
|
+
* "amount": 5000,
|
|
180
|
+
* "status": "active"
|
|
181
|
+
* }
|
|
182
|
+
* }
|
|
183
|
+
*/
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Example 5: Create a new contract
|
|
187
|
+
*
|
|
188
|
+
* Request: POST /api/data/contract
|
|
189
|
+
* Body: { "name": "Contract F", "amount": 4500, "status": "pending" }
|
|
190
|
+
*
|
|
191
|
+
* Response:
|
|
192
|
+
* {
|
|
193
|
+
* "data": {
|
|
194
|
+
* "id": "6",
|
|
195
|
+
* "name": "Contract F",
|
|
196
|
+
* "amount": 4500,
|
|
197
|
+
* "status": "pending"
|
|
198
|
+
* }
|
|
199
|
+
* }
|
|
200
|
+
*/
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Example 6: Update a contract
|
|
204
|
+
*
|
|
205
|
+
* Request: PUT /api/data/contract/1
|
|
206
|
+
* Body: { "status": "completed" }
|
|
207
|
+
*
|
|
208
|
+
* Response:
|
|
209
|
+
* {
|
|
210
|
+
* "data": {
|
|
211
|
+
* "id": "1",
|
|
212
|
+
* "name": "Contract A",
|
|
213
|
+
* "amount": 5000,
|
|
214
|
+
* "status": "completed"
|
|
215
|
+
* }
|
|
216
|
+
* }
|
|
217
|
+
*/
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Example 7: Delete a contract
|
|
221
|
+
*
|
|
222
|
+
* Request: DELETE /api/data/contract/1
|
|
223
|
+
*
|
|
224
|
+
* Response:
|
|
225
|
+
* {
|
|
226
|
+
* "data": {
|
|
227
|
+
* "id": "1",
|
|
228
|
+
* "deleted": true
|
|
229
|
+
* }
|
|
230
|
+
* }
|
|
231
|
+
*/
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Example 8: Error response (not found)
|
|
235
|
+
*
|
|
236
|
+
* Request: GET /api/data/contract/999
|
|
237
|
+
*
|
|
238
|
+
* Response:
|
|
239
|
+
* {
|
|
240
|
+
* "error": {
|
|
241
|
+
* "code": "NOT_FOUND",
|
|
242
|
+
* "message": "Record not found"
|
|
243
|
+
* }
|
|
244
|
+
* }
|
|
245
|
+
*/
|
|
246
|
+
|
|
247
|
+
export { app, driver };
|
package/test/node.test.ts
CHANGED
|
@@ -60,7 +60,7 @@ describe('Node Adapter', () => {
|
|
|
60
60
|
|
|
61
61
|
expect(response.status).toBe(200);
|
|
62
62
|
expect(response.body).toEqual({
|
|
63
|
-
|
|
63
|
+
items: [{ id: 1, name: 'Alice' }]
|
|
64
64
|
});
|
|
65
65
|
});
|
|
66
66
|
|
|
@@ -78,7 +78,9 @@ describe('Node Adapter', () => {
|
|
|
78
78
|
|
|
79
79
|
expect(response.status).toBe(200);
|
|
80
80
|
expect(response.body).toEqual({
|
|
81
|
-
|
|
81
|
+
id: 2,
|
|
82
|
+
name: 'Bob',
|
|
83
|
+
'@type': 'user'
|
|
82
84
|
});
|
|
83
85
|
});
|
|
84
86
|
|
package/test/rest.test.ts
CHANGED
|
@@ -17,7 +17,19 @@ class MockDriver implements Driver {
|
|
|
17
17
|
async init() {}
|
|
18
18
|
|
|
19
19
|
async find(objectName: string, query: any) {
|
|
20
|
-
|
|
20
|
+
let items = this.data[objectName] || [];
|
|
21
|
+
|
|
22
|
+
// Apply skip and limit if provided
|
|
23
|
+
if (query) {
|
|
24
|
+
if (query.skip) {
|
|
25
|
+
items = items.slice(query.skip);
|
|
26
|
+
}
|
|
27
|
+
if (query.limit) {
|
|
28
|
+
items = items.slice(0, query.limit);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return items;
|
|
21
33
|
}
|
|
22
34
|
|
|
23
35
|
async findOne(objectName: string, id: string | number, query?: any, options?: any) {
|
|
@@ -101,8 +113,8 @@ describe('REST API Adapter', () => {
|
|
|
101
113
|
.set('Accept', 'application/json');
|
|
102
114
|
|
|
103
115
|
expect(response.status).toBe(200);
|
|
104
|
-
expect(response.body.
|
|
105
|
-
expect(response.body.
|
|
116
|
+
expect(response.body.items).toHaveLength(2);
|
|
117
|
+
expect(response.body.items[0].name).toBe('Alice');
|
|
106
118
|
});
|
|
107
119
|
|
|
108
120
|
it('should handle GET /api/data/:object/:id - Get single record', async () => {
|
|
@@ -111,7 +123,8 @@ describe('REST API Adapter', () => {
|
|
|
111
123
|
.set('Accept', 'application/json');
|
|
112
124
|
|
|
113
125
|
expect(response.status).toBe(200);
|
|
114
|
-
expect(response.body.
|
|
126
|
+
expect(response.body.name).toBe('Alice');
|
|
127
|
+
expect(response.body['@type']).toBe('user');
|
|
115
128
|
});
|
|
116
129
|
|
|
117
130
|
it('should handle POST /api/data/:object - Create record', async () => {
|
|
@@ -121,8 +134,9 @@ describe('REST API Adapter', () => {
|
|
|
121
134
|
.set('Accept', 'application/json');
|
|
122
135
|
|
|
123
136
|
expect(response.status).toBe(201);
|
|
124
|
-
expect(response.body.
|
|
125
|
-
expect(response.body.
|
|
137
|
+
expect(response.body.name).toBe('Charlie');
|
|
138
|
+
expect(response.body._id).toBeDefined();
|
|
139
|
+
expect(response.body['@type']).toBe('user');
|
|
126
140
|
});
|
|
127
141
|
|
|
128
142
|
it('should handle PUT /api/data/:object/:id - Update record', async () => {
|
|
@@ -140,7 +154,8 @@ describe('REST API Adapter', () => {
|
|
|
140
154
|
.set('Accept', 'application/json');
|
|
141
155
|
|
|
142
156
|
expect(response.status).toBe(200);
|
|
143
|
-
expect(response.body.
|
|
157
|
+
expect(response.body.deleted).toBe(true);
|
|
158
|
+
expect(response.body['@type']).toBe('user');
|
|
144
159
|
});
|
|
145
160
|
|
|
146
161
|
it('should return 404 for non-existent object', async () => {
|
|
@@ -161,4 +176,29 @@ describe('REST API Adapter', () => {
|
|
|
161
176
|
expect(response.status).toBe(400);
|
|
162
177
|
expect(response.body.error.code).toBe('INVALID_REQUEST');
|
|
163
178
|
});
|
|
179
|
+
|
|
180
|
+
it('should include pagination metadata with limit and skip', async () => {
|
|
181
|
+
const response = await request(server)
|
|
182
|
+
.get('/api/data/user?limit=1&skip=0')
|
|
183
|
+
.set('Accept', 'application/json');
|
|
184
|
+
|
|
185
|
+
expect(response.status).toBe(200);
|
|
186
|
+
expect(response.body.items).toBeDefined();
|
|
187
|
+
expect(response.body.meta).toBeDefined();
|
|
188
|
+
expect(response.body.meta.total).toBe(2);
|
|
189
|
+
expect(response.body.meta.page).toBe(1);
|
|
190
|
+
expect(response.body.meta.size).toBe(1);
|
|
191
|
+
expect(response.body.meta.pages).toBe(2);
|
|
192
|
+
expect(response.body.meta.has_next).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should calculate pagination metadata correctly for second page', async () => {
|
|
196
|
+
const response = await request(server)
|
|
197
|
+
.get('/api/data/user?limit=1&skip=1')
|
|
198
|
+
.set('Accept', 'application/json');
|
|
199
|
+
|
|
200
|
+
expect(response.status).toBe(200);
|
|
201
|
+
expect(response.body.meta.page).toBe(2);
|
|
202
|
+
expect(response.body.meta.has_next).toBe(false);
|
|
203
|
+
});
|
|
164
204
|
});
|