json-rest-api 1.0.14 → 1.0.15
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/connectors_ideas/fastify.js +66 -68
- package/connectors_ideas/koa-plugin.js +50 -51
- package/eslint.config.js +11 -2
- package/examples/url-override-example.js +63 -63
- package/index.js +13 -13
- package/lib/enhanced-logger.js +51 -51
- package/lib/error-formatter.js +61 -61
- package/lib/rest-api-errors.js +28 -28
- package/package.json +1 -1
- package/plugins/core/bulk-operations-plugin.js +142 -145
- package/plugins/core/connectors/express-plugin.js +210 -217
- package/plugins/core/connectors/lib/busboy-detector.js +84 -84
- package/plugins/core/connectors/lib/formidable-detector.js +50 -50
- package/plugins/core/connectors/lib/request-helpers.js +74 -74
- package/plugins/core/file-handling-plugin.js +125 -125
- package/plugins/core/lib/anyapi/anyapi-registry.js +254 -254
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +287 -287
- package/plugins/core/lib/anyapi/schema-utils.js +101 -101
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +41 -41
- package/plugins/core/lib/dbTablesOperations.js +134 -135
- package/plugins/core/lib/fractional-positioning.js +74 -76
- package/plugins/core/lib/querying/knex-cross-table-search.js +320 -320
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +144 -144
- package/plugins/core/lib/querying/knex-pagination-helpers.js +164 -164
- package/plugins/core/lib/querying/knex-process-includes.js +50 -50
- package/plugins/core/lib/querying/knex-query-helpers-base.js +31 -31
- package/plugins/core/lib/querying/knex-query-helpers.js +451 -452
- package/plugins/core/lib/querying/knex-relationship-includes.js +1160 -1170
- package/plugins/core/lib/querying/knex-window-queries.js +89 -89
- package/plugins/core/lib/querying/url-helpers.js +35 -35
- package/plugins/core/lib/querying-writing/compile-schemas.js +97 -98
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +31 -31
- package/plugins/core/lib/querying-writing/database-capabilities.js +94 -94
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +76 -76
- package/plugins/core/lib/querying-writing/default-data-helpers.js +27 -27
- package/plugins/core/lib/querying-writing/field-utils.js +44 -42
- package/plugins/core/lib/querying-writing/knex-constants.js +15 -16
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +117 -115
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +41 -41
- package/plugins/core/lib/querying-writing/payload-validators.js +275 -275
- package/plugins/core/lib/querying-writing/schema-helpers.js +129 -129
- package/plugins/core/lib/querying-writing/scope-validations.js +143 -146
- package/plugins/core/lib/querying-writing/simplified-helpers.js +133 -129
- package/plugins/core/lib/storage/storage-adapter.js +68 -68
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +31 -31
- package/plugins/core/lib/writing/many-to-many-manipulations.js +81 -81
- package/plugins/core/lib/writing/relationship-processor.js +58 -58
- package/plugins/core/rest-api-access.js +8 -8
- package/plugins/core/rest-api-anyapi-knex-plugin.js +920 -920
- package/plugins/core/rest-api-cors-plugin.js +170 -171
- package/plugins/core/rest-api-knex-plugin.js +482 -493
- package/plugins/core/rest-api-multihome-plugin.js +247 -251
- package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +20 -21
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +44 -44
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +23 -23
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +15 -15
- package/plugins/core/rest-api-plugin-methods/add-route.js +9 -9
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +5 -6
- package/plugins/core/rest-api-plugin-methods/common.js +295 -297
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +35 -37
- package/plugins/core/rest-api-plugin-methods/delete.js +57 -59
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +172 -171
- package/plugins/core/rest-api-plugin-methods/get-related.js +96 -96
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +16 -18
- package/plugins/core/rest-api-plugin-methods/get.js +86 -86
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +16 -17
- package/plugins/core/rest-api-plugin-methods/patch.js +63 -64
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +54 -55
- package/plugins/core/rest-api-plugin-methods/post.js +61 -62
- package/plugins/core/rest-api-plugin-methods/put.js +86 -87
- package/plugins/core/rest-api-plugin-methods/query.js +88 -88
- package/plugins/core/rest-api-plugin-methods/release.js +5 -6
- package/plugins/core/rest-api-plugin.js +76 -78
- package/plugins/core/rest-api-positioning-plugin.js +251 -252
- package/plugins/core/socketio-plugin.js +240 -236
- package/plugins/storage/local-storage.js +103 -103
- package/plugins/storage/s3-storage.js +41 -42
- package/quickTest.js +37 -37
- package/scripts/debug-anyapi-review-filter.mjs +18 -18
- package/scripts/inspect-anyapi-descriptor-country.mjs +10 -10
- package/scripts/inspect-anyapi-descriptor.mjs +10 -10
- package/scripts/run-docs.js +112 -113
- package/test-debug.js +28 -28
- package/tests/TEST_TEMPLATE.test.js +51 -51
- package/tests/anyapi-basic.test.js +42 -42
- package/tests/anyapi-cursor-pagination.test.js +48 -48
- package/tests/bulk-operations.test.js +208 -208
- package/tests/computed-fields.test.js +204 -204
- package/tests/cors.test.js +412 -413
- package/tests/curl.test.js +275 -276
- package/tests/custom-idproperty-relationships.test.js +266 -266
- package/tests/field-getters.test.js +132 -132
- package/tests/field-setters.test.js +110 -110
- package/tests/fixtures/api-configs.js +699 -699
- package/tests/helpers/storage-mode.js +32 -32
- package/tests/helpers/test-utils.js +76 -76
- package/tests/include-depth-validation.test.js +76 -76
- package/tests/includes.test.js +145 -145
- package/tests/multihome.test.js +175 -175
- package/tests/nested-includes.test.js +255 -255
- package/tests/pagination-cursor-multifield.test.js +164 -164
- package/tests/pagination-enhanced.test.js +225 -225
- package/tests/polymorphic-relationships.test.js +96 -96
- package/tests/polymorphic-simple.test.js +44 -44
- package/tests/positioning-advanced.test.js +226 -226
- package/tests/positioning-extended.test.js +399 -399
- package/tests/positioning-multifilter.test.js +204 -204
- package/tests/positioning.test.js +266 -266
- package/tests/queries.test.js +184 -184
- package/tests/query-limits.test.js +154 -155
- package/tests/relationship-endpoints.test.js +127 -127
- package/tests/relationships.test.js +178 -179
- package/tests/rest-api-access.test.js +86 -86
- package/tests/rest-api.test.js +103 -103
- package/tests/searchschema-merge.test.js +93 -93
- package/tests/socketio.test.js +326 -326
- package/tests/virtual-fields.test.js +164 -164
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { describe, it, before, after, beforeEach } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import knexLib from 'knex'
|
|
4
|
-
import {
|
|
5
|
-
validateJsonApiStructure,
|
|
6
|
-
cleanTables,
|
|
7
|
-
createJsonApiDocument,
|
|
1
|
+
import { describe, it, before, after, beforeEach } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import knexLib from 'knex'
|
|
4
|
+
import {
|
|
5
|
+
validateJsonApiStructure,
|
|
6
|
+
cleanTables,
|
|
7
|
+
createJsonApiDocument,
|
|
8
8
|
createRelationship,
|
|
9
|
-
resourceIdentifier
|
|
10
|
-
} from './helpers/test-utils.js'
|
|
11
|
-
import { createPaginationApi } from './fixtures/api-configs.js'
|
|
9
|
+
resourceIdentifier
|
|
10
|
+
} from './helpers/test-utils.js'
|
|
11
|
+
import { createPaginationApi } from './fixtures/api-configs.js'
|
|
12
12
|
|
|
13
13
|
// Create Knex instance for tests
|
|
14
14
|
const knex = knexLib({
|
|
@@ -17,83 +17,83 @@ const knex = knexLib({
|
|
|
17
17
|
filename: ':memory:'
|
|
18
18
|
},
|
|
19
19
|
useNullAsDefault: true
|
|
20
|
-
})
|
|
20
|
+
})
|
|
21
21
|
|
|
22
22
|
// API instance that persists across tests
|
|
23
|
-
let api
|
|
23
|
+
let api
|
|
24
24
|
|
|
25
25
|
describe('Enhanced Pagination Features', () => {
|
|
26
26
|
before(async () => {
|
|
27
27
|
// Initialize API once with pagination features
|
|
28
|
-
api = await createPaginationApi(knex)
|
|
29
|
-
})
|
|
28
|
+
api = await createPaginationApi(knex)
|
|
29
|
+
})
|
|
30
30
|
|
|
31
31
|
after(async () => {
|
|
32
32
|
// Close database connection
|
|
33
|
-
await knex.destroy()
|
|
34
|
-
})
|
|
33
|
+
await knex.destroy()
|
|
34
|
+
})
|
|
35
35
|
|
|
36
36
|
describe('Self Links', () => {
|
|
37
37
|
beforeEach(async () => {
|
|
38
38
|
await cleanTables(knex, [
|
|
39
39
|
'pagination_countries', 'pagination_publishers', 'pagination_books'
|
|
40
|
-
])
|
|
40
|
+
])
|
|
41
41
|
|
|
42
42
|
// Create test data
|
|
43
|
-
const countryDoc = createJsonApiDocument('countries', { name: 'Test Country', code: 'TC' })
|
|
43
|
+
const countryDoc = createJsonApiDocument('countries', { name: 'Test Country', code: 'TC' })
|
|
44
44
|
const countryResult = await api.resources.countries.post({
|
|
45
45
|
inputRecord: countryDoc,
|
|
46
46
|
simplified: false
|
|
47
|
-
})
|
|
47
|
+
})
|
|
48
48
|
|
|
49
49
|
// Create some books
|
|
50
50
|
for (let i = 1; i <= 3; i++) {
|
|
51
|
-
const doc = createJsonApiDocument('books',
|
|
51
|
+
const doc = createJsonApiDocument('books',
|
|
52
52
|
{ title: `Book ${i}` },
|
|
53
53
|
{ country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
|
|
54
|
-
)
|
|
54
|
+
)
|
|
55
55
|
await api.resources.books.post({
|
|
56
56
|
inputRecord: doc,
|
|
57
57
|
simplified: false
|
|
58
|
-
})
|
|
58
|
+
})
|
|
59
59
|
}
|
|
60
|
-
})
|
|
60
|
+
})
|
|
61
61
|
|
|
62
62
|
it('should add self links to individual resources', async () => {
|
|
63
|
-
const list = await api.resources.books.query({ simplified: false })
|
|
64
|
-
const firstId = list.data[0]?.id
|
|
63
|
+
const list = await api.resources.books.query({ simplified: false })
|
|
64
|
+
const firstId = list.data[0]?.id
|
|
65
65
|
|
|
66
66
|
const result = await api.resources.books.get({
|
|
67
67
|
id: firstId,
|
|
68
68
|
simplified: false
|
|
69
|
-
})
|
|
69
|
+
})
|
|
70
70
|
|
|
71
|
-
validateJsonApiStructure(result, false)
|
|
72
|
-
assert(result.data.links, 'Resource should have links')
|
|
73
|
-
assert.equal(result.data.links.self, `/books/${firstId}`)
|
|
74
|
-
assert(result.links, 'Response should have top-level links')
|
|
75
|
-
assert.equal(result.links.self, `/books/${firstId}`)
|
|
76
|
-
})
|
|
71
|
+
validateJsonApiStructure(result, false)
|
|
72
|
+
assert(result.data.links, 'Resource should have links')
|
|
73
|
+
assert.equal(result.data.links.self, `/books/${firstId}`)
|
|
74
|
+
assert(result.links, 'Response should have top-level links')
|
|
75
|
+
assert.equal(result.links.self, `/books/${firstId}`)
|
|
76
|
+
})
|
|
77
77
|
|
|
78
78
|
it('should add self links to collection resources', async () => {
|
|
79
79
|
const result = await api.resources.books.query({
|
|
80
80
|
queryParams: {},
|
|
81
81
|
simplified: false
|
|
82
|
-
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
validateJsonApiStructure(result, true)
|
|
83
85
|
|
|
84
|
-
validateJsonApiStructure(result, true);
|
|
85
|
-
|
|
86
86
|
// Check individual resource links
|
|
87
87
|
result.data.forEach(book => {
|
|
88
|
-
assert(book.links, 'Each resource should have links')
|
|
89
|
-
assert(book.links.self, 'Each resource should have self link')
|
|
90
|
-
assert(book.links.self.startsWith('/books/'))
|
|
91
|
-
})
|
|
88
|
+
assert(book.links, 'Each resource should have links')
|
|
89
|
+
assert(book.links.self, 'Each resource should have self link')
|
|
90
|
+
assert(book.links.self.startsWith('/books/'))
|
|
91
|
+
})
|
|
92
92
|
|
|
93
93
|
// Check top-level links
|
|
94
|
-
assert(result.links, 'Response should have top-level links')
|
|
95
|
-
assert.equal(result.links.self, '/books')
|
|
96
|
-
})
|
|
94
|
+
assert(result.links, 'Response should have top-level links')
|
|
95
|
+
assert.equal(result.links.self, '/books')
|
|
96
|
+
})
|
|
97
97
|
|
|
98
98
|
it('should add self links to included resources', async () => {
|
|
99
99
|
const result = await api.resources.books.query({
|
|
@@ -101,63 +101,63 @@ describe('Enhanced Pagination Features', () => {
|
|
|
101
101
|
include: ['country']
|
|
102
102
|
},
|
|
103
103
|
simplified: false
|
|
104
|
-
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
validateJsonApiStructure(result, true)
|
|
107
|
+
assert(result.included, 'Should have included resources')
|
|
105
108
|
|
|
106
|
-
validateJsonApiStructure(result, true);
|
|
107
|
-
assert(result.included, 'Should have included resources');
|
|
108
|
-
|
|
109
109
|
result.included.forEach(resource => {
|
|
110
|
-
assert(resource.links, 'Included resource should have links')
|
|
111
|
-
assert(resource.links.self, 'Included resource should have self link')
|
|
112
|
-
assert(resource.links.self.startsWith('/'))
|
|
113
|
-
})
|
|
114
|
-
})
|
|
110
|
+
assert(resource.links, 'Included resource should have links')
|
|
111
|
+
assert(resource.links.self, 'Included resource should have self link')
|
|
112
|
+
assert(resource.links.self.startsWith('/'))
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
115
|
|
|
116
116
|
it('should add relative self links when no URL prefix configured', async () => {
|
|
117
117
|
// URLs are now always relative unless overridden via context
|
|
118
118
|
const result = await api.resources.books.query({
|
|
119
119
|
queryParams: {},
|
|
120
120
|
simplified: false
|
|
121
|
-
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
validateJsonApiStructure(result, true)
|
|
122
124
|
|
|
123
|
-
validateJsonApiStructure(result, true);
|
|
124
|
-
|
|
125
125
|
// Links should be present and relative
|
|
126
126
|
result.data.forEach(book => {
|
|
127
|
-
assert(book.links, 'Resources should have links')
|
|
128
|
-
assert(book.links.self, 'Each resource should have self link')
|
|
129
|
-
assert(book.links.self.startsWith('/books/'), 'Self link should be relative')
|
|
130
|
-
})
|
|
131
|
-
assert(result.links, 'Response should have top-level links')
|
|
132
|
-
assert(result.links.self.startsWith('/books'), 'Top-level self link should be relative')
|
|
133
|
-
})
|
|
134
|
-
})
|
|
127
|
+
assert(book.links, 'Resources should have links')
|
|
128
|
+
assert(book.links.self, 'Each resource should have self link')
|
|
129
|
+
assert(book.links.self.startsWith('/books/'), 'Self link should be relative')
|
|
130
|
+
})
|
|
131
|
+
assert(result.links, 'Response should have top-level links')
|
|
132
|
+
assert(result.links.self.startsWith('/books'), 'Top-level self link should be relative')
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
135
|
|
|
136
136
|
describe('Pagination Metadata', () => {
|
|
137
137
|
beforeEach(async () => {
|
|
138
138
|
await cleanTables(knex, [
|
|
139
139
|
'pagination_countries', 'pagination_publishers', 'pagination_books'
|
|
140
|
-
])
|
|
140
|
+
])
|
|
141
141
|
|
|
142
142
|
// Create test data
|
|
143
|
-
const countryDoc = createJsonApiDocument('countries', { name: 'Test Country', code: 'TC' })
|
|
143
|
+
const countryDoc = createJsonApiDocument('countries', { name: 'Test Country', code: 'TC' })
|
|
144
144
|
const countryResult = await api.resources.countries.post({
|
|
145
145
|
inputRecord: countryDoc,
|
|
146
146
|
simplified: false
|
|
147
|
-
})
|
|
147
|
+
})
|
|
148
148
|
|
|
149
149
|
// Create 10 books for pagination testing
|
|
150
150
|
for (let i = 1; i <= 10; i++) {
|
|
151
|
-
const doc = createJsonApiDocument('books',
|
|
151
|
+
const doc = createJsonApiDocument('books',
|
|
152
152
|
{ title: `Book ${String(i).padStart(2, '0')}` },
|
|
153
153
|
{ country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
|
|
154
|
-
)
|
|
154
|
+
)
|
|
155
155
|
await api.resources.books.post({
|
|
156
156
|
inputRecord: doc,
|
|
157
157
|
simplified: false
|
|
158
|
-
})
|
|
158
|
+
})
|
|
159
159
|
}
|
|
160
|
-
})
|
|
160
|
+
})
|
|
161
161
|
|
|
162
162
|
it('should include pagination metadata in response', async () => {
|
|
163
163
|
const result = await api.resources.books.query({
|
|
@@ -165,19 +165,19 @@ describe('Enhanced Pagination Features', () => {
|
|
|
165
165
|
page: { number: 2, size: 3 }
|
|
166
166
|
},
|
|
167
167
|
simplified: false
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
validateJsonApiStructure(result, true)
|
|
171
|
-
assert(result.meta, 'Response should have meta')
|
|
172
|
-
assert(result.meta.pagination, 'Meta should have pagination')
|
|
173
|
-
|
|
174
|
-
const pagination = result.meta.pagination
|
|
175
|
-
assert.equal(pagination.page, 2)
|
|
176
|
-
assert.equal(pagination.pageSize, 3)
|
|
177
|
-
assert.equal(pagination.total, 10)
|
|
178
|
-
assert.equal(pagination.pageCount, 4)
|
|
179
|
-
assert.equal(pagination.hasMore, true)
|
|
180
|
-
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
validateJsonApiStructure(result, true)
|
|
171
|
+
assert(result.meta, 'Response should have meta')
|
|
172
|
+
assert(result.meta.pagination, 'Meta should have pagination')
|
|
173
|
+
|
|
174
|
+
const pagination = result.meta.pagination
|
|
175
|
+
assert.equal(pagination.page, 2)
|
|
176
|
+
assert.equal(pagination.pageSize, 3)
|
|
177
|
+
assert.equal(pagination.total, 10)
|
|
178
|
+
assert.equal(pagination.pageCount, 4)
|
|
179
|
+
assert.equal(pagination.hasMore, true)
|
|
180
|
+
})
|
|
181
181
|
|
|
182
182
|
it('should calculate hasMore correctly for last page', async () => {
|
|
183
183
|
const result = await api.resources.books.query({
|
|
@@ -185,12 +185,12 @@ describe('Enhanced Pagination Features', () => {
|
|
|
185
185
|
page: { number: 4, size: 3 }
|
|
186
186
|
},
|
|
187
187
|
simplified: false
|
|
188
|
-
})
|
|
188
|
+
})
|
|
189
189
|
|
|
190
|
-
assert.equal(result.meta.pagination.hasMore, false)
|
|
191
|
-
assert.equal(result.meta.pagination.page, 4)
|
|
192
|
-
assert.equal(result.meta.pagination.pageCount, 4)
|
|
193
|
-
})
|
|
190
|
+
assert.equal(result.meta.pagination.hasMore, false)
|
|
191
|
+
assert.equal(result.meta.pagination.page, 4)
|
|
192
|
+
assert.equal(result.meta.pagination.pageCount, 4)
|
|
193
|
+
})
|
|
194
194
|
|
|
195
195
|
it('should include pagination links', async () => {
|
|
196
196
|
const result = await api.resources.books.query({
|
|
@@ -198,23 +198,23 @@ describe('Enhanced Pagination Features', () => {
|
|
|
198
198
|
page: { number: 2, size: 3 }
|
|
199
199
|
},
|
|
200
200
|
simplified: false
|
|
201
|
-
})
|
|
201
|
+
})
|
|
202
202
|
|
|
203
|
-
assert(result.links, 'Response should have links')
|
|
204
|
-
assert(result.links.self, 'Should have self link')
|
|
205
|
-
assert(result.links.first, 'Should have first link')
|
|
206
|
-
assert(result.links.last, 'Should have last link')
|
|
207
|
-
assert(result.links.prev, 'Should have prev link')
|
|
208
|
-
assert(result.links.next, 'Should have next link')
|
|
203
|
+
assert(result.links, 'Response should have links')
|
|
204
|
+
assert(result.links.self, 'Should have self link')
|
|
205
|
+
assert(result.links.first, 'Should have first link')
|
|
206
|
+
assert(result.links.last, 'Should have last link')
|
|
207
|
+
assert(result.links.prev, 'Should have prev link')
|
|
208
|
+
assert(result.links.next, 'Should have next link')
|
|
209
209
|
|
|
210
210
|
// Verify link structure
|
|
211
|
-
assert(result.links.self.includes('page[number]=2'))
|
|
212
|
-
assert(result.links.self.includes('page[size]=3'))
|
|
213
|
-
assert(result.links.first.includes('page[number]=1'))
|
|
214
|
-
assert(result.links.last.includes('page[number]=4'))
|
|
215
|
-
assert(result.links.prev.includes('page[number]=1'))
|
|
216
|
-
assert(result.links.next.includes('page[number]=3'))
|
|
217
|
-
})
|
|
211
|
+
assert(result.links.self.includes('page[number]=2'))
|
|
212
|
+
assert(result.links.self.includes('page[size]=3'))
|
|
213
|
+
assert(result.links.first.includes('page[number]=1'))
|
|
214
|
+
assert(result.links.last.includes('page[number]=4'))
|
|
215
|
+
assert(result.links.prev.includes('page[number]=1'))
|
|
216
|
+
assert(result.links.next.includes('page[number]=3'))
|
|
217
|
+
})
|
|
218
218
|
|
|
219
219
|
it('should not include prev link on first page', async () => {
|
|
220
220
|
const result = await api.resources.books.query({
|
|
@@ -222,12 +222,12 @@ describe('Enhanced Pagination Features', () => {
|
|
|
222
222
|
page: { number: 1, size: 3 }
|
|
223
223
|
},
|
|
224
224
|
simplified: false
|
|
225
|
-
})
|
|
225
|
+
})
|
|
226
226
|
|
|
227
|
-
assert(result.links.first)
|
|
228
|
-
assert(result.links.next)
|
|
229
|
-
assert(!result.links.prev, 'First page should not have prev link')
|
|
230
|
-
})
|
|
227
|
+
assert(result.links.first)
|
|
228
|
+
assert(result.links.next)
|
|
229
|
+
assert(!result.links.prev, 'First page should not have prev link')
|
|
230
|
+
})
|
|
231
231
|
|
|
232
232
|
it('should not include next link on last page', async () => {
|
|
233
233
|
const result = await api.resources.books.query({
|
|
@@ -235,12 +235,12 @@ describe('Enhanced Pagination Features', () => {
|
|
|
235
235
|
page: { number: 4, size: 3 }
|
|
236
236
|
},
|
|
237
237
|
simplified: false
|
|
238
|
-
})
|
|
238
|
+
})
|
|
239
239
|
|
|
240
|
-
assert(result.links.first)
|
|
241
|
-
assert(result.links.prev)
|
|
242
|
-
assert(!result.links.next, 'Last page should not have next link')
|
|
243
|
-
})
|
|
240
|
+
assert(result.links.first)
|
|
241
|
+
assert(result.links.prev)
|
|
242
|
+
assert(!result.links.next, 'Last page should not have next link')
|
|
243
|
+
})
|
|
244
244
|
|
|
245
245
|
it('should preserve other query parameters in pagination links', async () => {
|
|
246
246
|
const result = await api.resources.books.query({
|
|
@@ -249,43 +249,43 @@ describe('Enhanced Pagination Features', () => {
|
|
|
249
249
|
sort: ['-title']
|
|
250
250
|
},
|
|
251
251
|
simplified: false
|
|
252
|
-
})
|
|
252
|
+
})
|
|
253
253
|
|
|
254
|
-
assert(result.links, 'Should have links')
|
|
255
|
-
assert(result.links.next, 'Should have next link since page 2 of 4')
|
|
256
|
-
assert(result.links.next.includes('sort=-title'))
|
|
257
|
-
assert(result.links.next.includes('page[number]=3'))
|
|
258
|
-
assert(result.links.next.includes('page[size]=3'))
|
|
259
|
-
})
|
|
260
|
-
})
|
|
254
|
+
assert(result.links, 'Should have links')
|
|
255
|
+
assert(result.links.next, 'Should have next link since page 2 of 4')
|
|
256
|
+
assert(result.links.next.includes('sort=-title'))
|
|
257
|
+
assert(result.links.next.includes('page[number]=3'))
|
|
258
|
+
assert(result.links.next.includes('page[size]=3'))
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
261
|
|
|
262
262
|
describe('Cursor-based Pagination', () => {
|
|
263
263
|
beforeEach(async () => {
|
|
264
264
|
await cleanTables(knex, [
|
|
265
265
|
'pagination_countries', 'pagination_publishers', 'pagination_books'
|
|
266
|
-
])
|
|
266
|
+
])
|
|
267
267
|
|
|
268
268
|
// Create test data
|
|
269
|
-
const countryDoc = createJsonApiDocument('countries', { name: 'Test Country', code: 'TC' })
|
|
269
|
+
const countryDoc = createJsonApiDocument('countries', { name: 'Test Country', code: 'TC' })
|
|
270
270
|
const countryResult = await api.resources.countries.post({
|
|
271
271
|
inputRecord: countryDoc,
|
|
272
272
|
simplified: false
|
|
273
|
-
})
|
|
273
|
+
})
|
|
274
274
|
|
|
275
275
|
// Create books
|
|
276
276
|
for (let i = 1; i <= 15; i++) {
|
|
277
|
-
const doc = createJsonApiDocument('books',
|
|
278
|
-
{
|
|
277
|
+
const doc = createJsonApiDocument('books',
|
|
278
|
+
{
|
|
279
279
|
title: `Book ${String(i).padStart(2, '0')}`
|
|
280
280
|
},
|
|
281
281
|
{ country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
|
|
282
|
-
)
|
|
282
|
+
)
|
|
283
283
|
await api.resources.books.post({
|
|
284
284
|
inputRecord: doc,
|
|
285
285
|
simplified: false
|
|
286
|
-
})
|
|
286
|
+
})
|
|
287
287
|
}
|
|
288
|
-
})
|
|
288
|
+
})
|
|
289
289
|
|
|
290
290
|
it('should support cursor-based pagination with after parameter', async () => {
|
|
291
291
|
// Get first page
|
|
@@ -295,33 +295,33 @@ describe('Enhanced Pagination Features', () => {
|
|
|
295
295
|
sort: ['-id']
|
|
296
296
|
},
|
|
297
297
|
simplified: false
|
|
298
|
-
})
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
validateJsonApiStructure(firstPage, true)
|
|
301
|
+
assert.equal(firstPage.data.length, 5)
|
|
302
|
+
assert(firstPage.meta.pagination.hasMore)
|
|
303
|
+
assert(firstPage.meta.pagination.cursor?.next)
|
|
299
304
|
|
|
300
|
-
validateJsonApiStructure(firstPage, true);
|
|
301
|
-
assert.equal(firstPage.data.length, 5);
|
|
302
|
-
assert(firstPage.meta.pagination.hasMore);
|
|
303
|
-
assert(firstPage.meta.pagination.cursor?.next);
|
|
304
|
-
|
|
305
305
|
// Get next page using cursor
|
|
306
306
|
const secondPage = await api.resources.books.query({
|
|
307
307
|
queryParams: {
|
|
308
|
-
page: {
|
|
308
|
+
page: {
|
|
309
309
|
size: 5,
|
|
310
310
|
after: firstPage.meta.pagination.cursor.next
|
|
311
311
|
},
|
|
312
312
|
sort: ['-id']
|
|
313
313
|
},
|
|
314
314
|
simplified: false
|
|
315
|
-
})
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
validateJsonApiStructure(secondPage, true)
|
|
318
|
+
assert.equal(secondPage.data.length, 5)
|
|
316
319
|
|
|
317
|
-
validateJsonApiStructure(secondPage, true);
|
|
318
|
-
assert.equal(secondPage.data.length, 5);
|
|
319
|
-
|
|
320
320
|
// Verify different records
|
|
321
|
-
const firstPageIds = firstPage.data.map(b => b.id)
|
|
322
|
-
const secondPageIds = secondPage.data.map(b => b.id)
|
|
323
|
-
assert(!firstPageIds.some(id => secondPageIds.includes(id)), 'Pages should have different records')
|
|
324
|
-
})
|
|
321
|
+
const firstPageIds = firstPage.data.map(b => b.id)
|
|
322
|
+
const secondPageIds = secondPage.data.map(b => b.id)
|
|
323
|
+
assert(!firstPageIds.some(id => secondPageIds.includes(id)), 'Pages should have different records')
|
|
324
|
+
})
|
|
325
325
|
|
|
326
326
|
it('should include cursor pagination links', async () => {
|
|
327
327
|
const result = await api.resources.books.query({
|
|
@@ -329,69 +329,69 @@ describe('Enhanced Pagination Features', () => {
|
|
|
329
329
|
page: { size: 5 }
|
|
330
330
|
},
|
|
331
331
|
simplified: false
|
|
332
|
-
})
|
|
332
|
+
})
|
|
333
333
|
|
|
334
|
-
assert(result.links)
|
|
335
|
-
assert(result.links.self)
|
|
336
|
-
assert(result.links.first)
|
|
337
|
-
assert(result.links.next)
|
|
338
|
-
assert(result.links.next.includes('page[after]='), 'Next link should include cursor parameter')
|
|
339
|
-
})
|
|
334
|
+
assert(result.links)
|
|
335
|
+
assert(result.links.self)
|
|
336
|
+
assert(result.links.first)
|
|
337
|
+
assert(result.links.next)
|
|
338
|
+
assert(result.links.next.includes('page[after]='), 'Next link should include cursor parameter')
|
|
339
|
+
})
|
|
340
340
|
|
|
341
341
|
it('should handle last page with cursor pagination', async () => {
|
|
342
342
|
// Get to last page
|
|
343
|
-
let currentCursor = null
|
|
344
|
-
let result
|
|
345
|
-
|
|
343
|
+
let currentCursor = null
|
|
344
|
+
let result
|
|
345
|
+
|
|
346
346
|
// Navigate through pages
|
|
347
347
|
for (let i = 0; i < 3; i++) {
|
|
348
348
|
result = await api.resources.books.query({
|
|
349
349
|
queryParams: {
|
|
350
|
-
page: {
|
|
350
|
+
page: {
|
|
351
351
|
size: 5,
|
|
352
352
|
...(currentCursor && { after: currentCursor })
|
|
353
353
|
}
|
|
354
354
|
},
|
|
355
355
|
simplified: false
|
|
356
|
-
})
|
|
357
|
-
|
|
356
|
+
})
|
|
357
|
+
|
|
358
358
|
if (result.meta.pagination.cursor?.next) {
|
|
359
|
-
currentCursor = result.meta.pagination.cursor.next
|
|
359
|
+
currentCursor = result.meta.pagination.cursor.next
|
|
360
360
|
}
|
|
361
361
|
}
|
|
362
362
|
|
|
363
363
|
// Last page should have no next cursor
|
|
364
|
-
assert.equal(result.meta.pagination.hasMore, false)
|
|
365
|
-
assert(!result.meta.pagination.cursor?.next)
|
|
366
|
-
assert(!result.links.next)
|
|
367
|
-
})
|
|
368
|
-
})
|
|
364
|
+
assert.equal(result.meta.pagination.hasMore, false)
|
|
365
|
+
assert(!result.meta.pagination.cursor?.next)
|
|
366
|
+
assert(!result.links.next)
|
|
367
|
+
})
|
|
368
|
+
})
|
|
369
369
|
|
|
370
370
|
describe('Count Query Configuration', () => {
|
|
371
371
|
beforeEach(async () => {
|
|
372
372
|
await cleanTables(knex, [
|
|
373
373
|
'pagination_countries', 'pagination_publishers', 'pagination_books'
|
|
374
|
-
])
|
|
374
|
+
])
|
|
375
375
|
|
|
376
376
|
// Create test data
|
|
377
|
-
const countryDoc = createJsonApiDocument('countries', { name: 'Test Country', code: 'TC' })
|
|
377
|
+
const countryDoc = createJsonApiDocument('countries', { name: 'Test Country', code: 'TC' })
|
|
378
378
|
const countryResult = await api.resources.countries.post({
|
|
379
379
|
inputRecord: countryDoc,
|
|
380
380
|
simplified: false
|
|
381
|
-
})
|
|
381
|
+
})
|
|
382
382
|
|
|
383
383
|
// Create books
|
|
384
384
|
for (let i = 1; i <= 20; i++) {
|
|
385
|
-
const doc = createJsonApiDocument('books',
|
|
385
|
+
const doc = createJsonApiDocument('books',
|
|
386
386
|
{ title: `Book ${i}` },
|
|
387
387
|
{ country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
|
|
388
|
-
)
|
|
388
|
+
)
|
|
389
389
|
await api.resources.books.post({
|
|
390
390
|
inputRecord: doc,
|
|
391
391
|
simplified: false
|
|
392
|
-
})
|
|
392
|
+
})
|
|
393
393
|
}
|
|
394
|
-
})
|
|
394
|
+
})
|
|
395
395
|
|
|
396
396
|
it('should include count by default', async () => {
|
|
397
397
|
const result = await api.resources.books.query({
|
|
@@ -399,75 +399,75 @@ describe('Enhanced Pagination Features', () => {
|
|
|
399
399
|
page: { number: 1, size: 5 }
|
|
400
400
|
},
|
|
401
401
|
simplified: false
|
|
402
|
-
})
|
|
402
|
+
})
|
|
403
403
|
|
|
404
|
-
assert(result.meta.pagination.total !== undefined)
|
|
405
|
-
assert.equal(result.meta.pagination.total, 20)
|
|
406
|
-
assert.equal(result.meta.pagination.pageCount, 4)
|
|
407
|
-
})
|
|
404
|
+
assert(result.meta.pagination.total !== undefined)
|
|
405
|
+
assert.equal(result.meta.pagination.total, 20)
|
|
406
|
+
assert.equal(result.meta.pagination.pageCount, 4)
|
|
407
|
+
})
|
|
408
408
|
|
|
409
409
|
it('should respect enablePaginationCounts when set to false', async () => {
|
|
410
410
|
// Override the enablePaginationCounts setting for this scope
|
|
411
|
-
const booksScope = api.resources.books
|
|
412
|
-
const originalSetting = booksScope.vars.enablePaginationCounts
|
|
413
|
-
booksScope.vars.enablePaginationCounts = false
|
|
414
|
-
|
|
411
|
+
const booksScope = api.resources.books
|
|
412
|
+
const originalSetting = booksScope.vars.enablePaginationCounts
|
|
413
|
+
booksScope.vars.enablePaginationCounts = false
|
|
414
|
+
|
|
415
415
|
const result = await api.resources.books.query({
|
|
416
416
|
queryParams: {
|
|
417
417
|
page: { number: 1, size: 5 }
|
|
418
418
|
},
|
|
419
419
|
simplified: false
|
|
420
|
-
})
|
|
421
|
-
|
|
422
|
-
assert(result.meta.pagination)
|
|
423
|
-
assert.equal(result.meta.pagination.page, 1)
|
|
424
|
-
assert.equal(result.meta.pagination.pageSize, 5)
|
|
425
|
-
assert(result.meta.pagination.total === undefined, 'Should not include total when counts disabled')
|
|
426
|
-
assert(result.meta.pagination.pageCount === undefined, 'Should not include pageCount when counts disabled')
|
|
427
|
-
assert(result.meta.pagination.hasMore === undefined, 'Should not include hasMore when counts disabled')
|
|
428
|
-
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
assert(result.meta.pagination)
|
|
423
|
+
assert.equal(result.meta.pagination.page, 1)
|
|
424
|
+
assert.equal(result.meta.pagination.pageSize, 5)
|
|
425
|
+
assert(result.meta.pagination.total === undefined, 'Should not include total when counts disabled')
|
|
426
|
+
assert(result.meta.pagination.pageCount === undefined, 'Should not include pageCount when counts disabled')
|
|
427
|
+
assert(result.meta.pagination.hasMore === undefined, 'Should not include hasMore when counts disabled')
|
|
428
|
+
|
|
429
429
|
// Restore the setting
|
|
430
|
-
booksScope.vars.enablePaginationCounts = originalSetting
|
|
431
|
-
})
|
|
432
|
-
})
|
|
430
|
+
booksScope.vars.enablePaginationCounts = originalSetting
|
|
431
|
+
})
|
|
432
|
+
})
|
|
433
433
|
|
|
434
434
|
describe('Combined Features', () => {
|
|
435
|
-
let usCountryId
|
|
436
|
-
|
|
435
|
+
let usCountryId
|
|
436
|
+
|
|
437
437
|
beforeEach(async () => {
|
|
438
438
|
await cleanTables(knex, [
|
|
439
439
|
'pagination_countries', 'pagination_publishers', 'pagination_books'
|
|
440
|
-
])
|
|
440
|
+
])
|
|
441
441
|
|
|
442
442
|
// Create multiple countries
|
|
443
|
-
const countries = []
|
|
443
|
+
const countries = []
|
|
444
444
|
for (const code of ['US', 'UK', 'FR']) {
|
|
445
|
-
const doc = createJsonApiDocument('countries', { name: `Country ${code}`, code })
|
|
445
|
+
const doc = createJsonApiDocument('countries', { name: `Country ${code}`, code })
|
|
446
446
|
const result = await api.resources.countries.post({
|
|
447
447
|
inputRecord: doc,
|
|
448
448
|
simplified: false
|
|
449
|
-
})
|
|
450
|
-
countries.push(result.data)
|
|
449
|
+
})
|
|
450
|
+
countries.push(result.data)
|
|
451
451
|
if (code === 'US') {
|
|
452
|
-
usCountryId = result.data.id
|
|
452
|
+
usCountryId = result.data.id
|
|
453
453
|
}
|
|
454
454
|
}
|
|
455
455
|
|
|
456
456
|
// Create books distributed across countries
|
|
457
|
-
let bookIndex = 1
|
|
457
|
+
let bookIndex = 1
|
|
458
458
|
for (const country of countries) {
|
|
459
459
|
for (let i = 0; i < 8; i++) {
|
|
460
|
-
const doc = createJsonApiDocument('books',
|
|
460
|
+
const doc = createJsonApiDocument('books',
|
|
461
461
|
{ title: `Book ${String(bookIndex++).padStart(2, '0')}` },
|
|
462
462
|
{ country: createRelationship(resourceIdentifier('countries', country.id)) }
|
|
463
|
-
)
|
|
463
|
+
)
|
|
464
464
|
await api.resources.books.post({
|
|
465
465
|
inputRecord: doc,
|
|
466
466
|
simplified: false
|
|
467
|
-
})
|
|
467
|
+
})
|
|
468
468
|
}
|
|
469
469
|
}
|
|
470
|
-
})
|
|
470
|
+
})
|
|
471
471
|
|
|
472
472
|
it('should combine pagination with filtering and includes', async () => {
|
|
473
473
|
const result = await api.resources.books.query({
|
|
@@ -480,33 +480,33 @@ describe('Enhanced Pagination Features', () => {
|
|
|
480
480
|
sort: ['title']
|
|
481
481
|
},
|
|
482
482
|
simplified: false
|
|
483
|
-
})
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
validateJsonApiStructure(result, true)
|
|
484
486
|
|
|
485
|
-
validateJsonApiStructure(result, true);
|
|
486
|
-
|
|
487
487
|
// Check pagination worked
|
|
488
|
-
assert.equal(result.data.length, 5)
|
|
489
|
-
assert.equal(result.meta.pagination.total, 8)
|
|
490
|
-
assert.equal(result.meta.pagination.pageCount, 2)
|
|
491
|
-
|
|
488
|
+
assert.equal(result.data.length, 5)
|
|
489
|
+
assert.equal(result.meta.pagination.total, 8)
|
|
490
|
+
assert.equal(result.meta.pagination.pageCount, 2)
|
|
491
|
+
|
|
492
492
|
// Check filtering worked
|
|
493
|
-
const countryIds = result.data.map(b => b.relationships.country.data.id)
|
|
494
|
-
assert(countryIds.every(id => id === countryIds[0]), 'All books should be from same country')
|
|
495
|
-
|
|
493
|
+
const countryIds = result.data.map(b => b.relationships.country.data.id)
|
|
494
|
+
assert(countryIds.every(id => id === countryIds[0]), 'All books should be from same country')
|
|
495
|
+
|
|
496
496
|
// Check includes worked
|
|
497
|
-
assert(result.included)
|
|
498
|
-
const usCountry = result.included.find(r => r.type === 'countries' && r.attributes.code === 'US')
|
|
499
|
-
assert(usCountry)
|
|
500
|
-
|
|
497
|
+
assert(result.included)
|
|
498
|
+
const usCountry = result.included.find(r => r.type === 'countries' && r.attributes.code === 'US')
|
|
499
|
+
assert(usCountry)
|
|
500
|
+
|
|
501
501
|
// Check self links present
|
|
502
502
|
result.data.forEach(book => {
|
|
503
|
-
assert(book.links.self)
|
|
504
|
-
})
|
|
505
|
-
|
|
503
|
+
assert(book.links.self)
|
|
504
|
+
})
|
|
505
|
+
|
|
506
506
|
// Check pagination links include all parameters
|
|
507
|
-
assert(result.links.next.includes(`filters[country]=${usCountryId}`))
|
|
508
|
-
assert(result.links.next.includes('include=country'))
|
|
509
|
-
assert(result.links.next.includes('sort=title'))
|
|
510
|
-
})
|
|
511
|
-
})
|
|
512
|
-
})
|
|
507
|
+
assert(result.links.next.includes(`filters[country]=${usCountryId}`))
|
|
508
|
+
assert(result.links.next.includes('include=country'))
|
|
509
|
+
assert(result.links.next.includes('sort=title'))
|
|
510
|
+
})
|
|
511
|
+
})
|
|
512
|
+
})
|