json-rest-api 1.0.14 → 1.0.16
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 +99 -99
- 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 +11 -15
- 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
package/tests/queries.test.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { describe, it, before, beforeEach, after } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import knexLib from 'knex'
|
|
4
|
-
import { createBasicApi, createExtendedApi } from './fixtures/api-configs.js'
|
|
1
|
+
import { describe, it, before, beforeEach, after } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import knexLib from 'knex'
|
|
4
|
+
import { createBasicApi, createExtendedApi } from './fixtures/api-configs.js'
|
|
5
5
|
import {
|
|
6
6
|
validateJsonApiStructure,
|
|
7
7
|
resourceIdentifier,
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
createToManyRelationship,
|
|
13
13
|
assertResourceAttributes,
|
|
14
14
|
assertResourceRelationship
|
|
15
|
-
} from './helpers/test-utils.js'
|
|
15
|
+
} from './helpers/test-utils.js'
|
|
16
16
|
|
|
17
17
|
// Create Knex instance for tests
|
|
18
18
|
const knex = knexLib({
|
|
@@ -21,75 +21,75 @@ const knex = knexLib({
|
|
|
21
21
|
filename: ':memory:'
|
|
22
22
|
},
|
|
23
23
|
useNullAsDefault: true
|
|
24
|
-
})
|
|
24
|
+
})
|
|
25
25
|
|
|
26
26
|
// API instances that persist across tests
|
|
27
|
-
let basicApi
|
|
28
|
-
let extendedApi
|
|
27
|
+
let basicApi
|
|
28
|
+
let extendedApi
|
|
29
29
|
|
|
30
30
|
describe('Query Operations', () => {
|
|
31
31
|
before(async () => {
|
|
32
32
|
// Initialize APIs once
|
|
33
|
-
basicApi = await createBasicApi(knex)
|
|
34
|
-
extendedApi = await createExtendedApi(knex)
|
|
35
|
-
})
|
|
33
|
+
basicApi = await createBasicApi(knex)
|
|
34
|
+
extendedApi = await createExtendedApi(knex)
|
|
35
|
+
})
|
|
36
36
|
|
|
37
37
|
after(async () => {
|
|
38
38
|
// Close database connection to allow tests to exit
|
|
39
|
-
await knex.destroy()
|
|
40
|
-
})
|
|
39
|
+
await knex.destroy()
|
|
40
|
+
})
|
|
41
41
|
|
|
42
42
|
describe('Filtering', () => {
|
|
43
|
-
|
|
43
|
+
const testData = {}
|
|
44
44
|
|
|
45
45
|
beforeEach(async () => {
|
|
46
46
|
// Clean all tables
|
|
47
47
|
await cleanTables(knex, [
|
|
48
48
|
'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors'
|
|
49
|
-
])
|
|
49
|
+
])
|
|
50
50
|
|
|
51
51
|
// Create test data set
|
|
52
52
|
// Create 3 countries
|
|
53
|
-
const countries = []
|
|
53
|
+
const countries = []
|
|
54
54
|
for (const [name, code] of [['United States', 'US'], ['United Kingdom', 'UK'], ['France', 'FR']]) {
|
|
55
|
-
const doc = createJsonApiDocument('countries', { name, code })
|
|
55
|
+
const doc = createJsonApiDocument('countries', { name, code })
|
|
56
56
|
const result = await basicApi.resources.countries.post({
|
|
57
57
|
inputRecord: doc,
|
|
58
58
|
simplified: false
|
|
59
|
-
})
|
|
60
|
-
countries.push(result.data)
|
|
59
|
+
})
|
|
60
|
+
countries.push(result.data)
|
|
61
61
|
}
|
|
62
|
-
testData.countries = countries
|
|
62
|
+
testData.countries = countries
|
|
63
63
|
|
|
64
64
|
// Create 2 publishers per country
|
|
65
|
-
const publishers = []
|
|
66
|
-
let pubIndex = 0
|
|
65
|
+
const publishers = []
|
|
66
|
+
let pubIndex = 0
|
|
67
67
|
for (const country of countries) {
|
|
68
68
|
for (let i = 0; i < 2; i++) {
|
|
69
69
|
const doc = createJsonApiDocument('publishers',
|
|
70
70
|
{ name: `Publisher ${++pubIndex}` },
|
|
71
71
|
{ country: createRelationship(resourceIdentifier('countries', country.id)) }
|
|
72
|
-
)
|
|
72
|
+
)
|
|
73
73
|
const result = await basicApi.resources.publishers.post({
|
|
74
74
|
inputRecord: doc,
|
|
75
75
|
simplified: false
|
|
76
|
-
})
|
|
77
|
-
publishers.push({ ...result.data, countryId: country.id })
|
|
76
|
+
})
|
|
77
|
+
publishers.push({ ...result.data, countryId: country.id })
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
-
testData.publishers = publishers
|
|
80
|
+
testData.publishers = publishers
|
|
81
81
|
|
|
82
82
|
// Create 3 authors
|
|
83
|
-
const authors = []
|
|
83
|
+
const authors = []
|
|
84
84
|
for (const name of ['Author One', 'Author Two', 'Author Three']) {
|
|
85
|
-
const doc = createJsonApiDocument('authors', { name })
|
|
85
|
+
const doc = createJsonApiDocument('authors', { name })
|
|
86
86
|
const result = await basicApi.resources.authors.post({
|
|
87
87
|
inputRecord: doc,
|
|
88
88
|
simplified: false
|
|
89
|
-
})
|
|
90
|
-
authors.push(result.data)
|
|
89
|
+
})
|
|
90
|
+
authors.push(result.data)
|
|
91
91
|
}
|
|
92
|
-
testData.authors = authors
|
|
92
|
+
testData.authors = authors
|
|
93
93
|
|
|
94
94
|
// Create 5 books with various relationships
|
|
95
95
|
const books = [
|
|
@@ -98,9 +98,9 @@ describe('Query Operations', () => {
|
|
|
98
98
|
{ title: 'Book C', countryId: countries[1].id, publisherId: publishers[2].id, authorIds: [authors[1].id] },
|
|
99
99
|
{ title: 'Book D', countryId: countries[1].id, publisherId: publishers[3].id, authorIds: [authors[2].id] },
|
|
100
100
|
{ title: 'Book E', countryId: countries[2].id, publisherId: publishers[4].id, authorIds: [authors[0].id, authors[2].id] }
|
|
101
|
-
]
|
|
101
|
+
]
|
|
102
102
|
|
|
103
|
-
testData.books = []
|
|
103
|
+
testData.books = []
|
|
104
104
|
for (const bookData of books) {
|
|
105
105
|
const doc = createJsonApiDocument('books',
|
|
106
106
|
{
|
|
@@ -113,14 +113,14 @@ describe('Query Operations', () => {
|
|
|
113
113
|
bookData.authorIds.map(id => resourceIdentifier('authors', id))
|
|
114
114
|
)
|
|
115
115
|
}
|
|
116
|
-
)
|
|
116
|
+
)
|
|
117
117
|
const result = await basicApi.resources.books.post({
|
|
118
118
|
inputRecord: doc,
|
|
119
119
|
simplified: false
|
|
120
|
-
})
|
|
121
|
-
testData.books.push(result.data)
|
|
120
|
+
})
|
|
121
|
+
testData.books.push(result.data)
|
|
122
122
|
}
|
|
123
|
-
})
|
|
123
|
+
})
|
|
124
124
|
|
|
125
125
|
it('should filter by simple field', async () => {
|
|
126
126
|
const result = await basicApi.resources.books.query({
|
|
@@ -128,20 +128,20 @@ describe('Query Operations', () => {
|
|
|
128
128
|
filters: { country: testData.countries[0].id }
|
|
129
129
|
},
|
|
130
130
|
simplified: false
|
|
131
|
-
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
validateJsonApiStructure(result, true)
|
|
134
|
+
assert.equal(result.data.length, 2, 'Should return 2 books from US')
|
|
132
135
|
|
|
133
|
-
validateJsonApiStructure(result, true);
|
|
134
|
-
assert.equal(result.data.length, 2, 'Should return 2 books from US');
|
|
135
|
-
|
|
136
136
|
// Verify all returned books are from the correct country
|
|
137
137
|
for (const book of result.data) {
|
|
138
|
-
const bookData = testData.books.find(b => b.id === book.id)
|
|
139
|
-
assert(bookData)
|
|
140
|
-
const originalBook = testData.books.find(b => b.id === book.id)
|
|
141
|
-
assert.equal(originalBook.attributes.title.startsWith('Book A') ||
|
|
142
|
-
originalBook.attributes.title.startsWith('Book B'), true)
|
|
138
|
+
const bookData = testData.books.find(b => b.id === book.id)
|
|
139
|
+
assert(bookData)
|
|
140
|
+
const originalBook = testData.books.find(b => b.id === book.id)
|
|
141
|
+
assert.equal(originalBook.attributes.title.startsWith('Book A') ||
|
|
142
|
+
originalBook.attributes.title.startsWith('Book B'), true)
|
|
143
143
|
}
|
|
144
|
-
})
|
|
144
|
+
})
|
|
145
145
|
|
|
146
146
|
it('should filter by multiple fields', async () => {
|
|
147
147
|
const result = await basicApi.resources.books.query({
|
|
@@ -152,12 +152,12 @@ describe('Query Operations', () => {
|
|
|
152
152
|
}
|
|
153
153
|
},
|
|
154
154
|
simplified: false
|
|
155
|
-
})
|
|
155
|
+
})
|
|
156
156
|
|
|
157
|
-
validateJsonApiStructure(result, true)
|
|
158
|
-
assert.equal(result.data.length, 1, 'Should return 1 book matching both filters')
|
|
159
|
-
assert.equal(result.data[0].attributes.title, 'Book A')
|
|
160
|
-
})
|
|
157
|
+
validateJsonApiStructure(result, true)
|
|
158
|
+
assert.equal(result.data.length, 1, 'Should return 1 book matching both filters')
|
|
159
|
+
assert.equal(result.data[0].attributes.title, 'Book A')
|
|
160
|
+
})
|
|
161
161
|
|
|
162
162
|
it('should return empty array when no matches', async () => {
|
|
163
163
|
const result = await basicApi.resources.books.query({
|
|
@@ -165,11 +165,11 @@ describe('Query Operations', () => {
|
|
|
165
165
|
filters: { country: 99999 }
|
|
166
166
|
},
|
|
167
167
|
simplified: false
|
|
168
|
-
})
|
|
168
|
+
})
|
|
169
169
|
|
|
170
|
-
validateJsonApiStructure(result, true)
|
|
171
|
-
assert.equal(result.data.length, 0, 'Should return empty array')
|
|
172
|
-
})
|
|
170
|
+
validateJsonApiStructure(result, true)
|
|
171
|
+
assert.equal(result.data.length, 0, 'Should return empty array')
|
|
172
|
+
})
|
|
173
173
|
|
|
174
174
|
it('should throw error when filtering on non-searchable field', async () => {
|
|
175
175
|
await assert.rejects(
|
|
@@ -179,44 +179,44 @@ describe('Query Operations', () => {
|
|
|
179
179
|
filters: { invalid_field: 'test' }
|
|
180
180
|
},
|
|
181
181
|
simplified: false
|
|
182
|
-
})
|
|
182
|
+
})
|
|
183
183
|
},
|
|
184
184
|
(err) => {
|
|
185
|
-
return err.code === 'REST_API_VALIDATION'
|
|
185
|
+
return err.code === 'REST_API_VALIDATION'
|
|
186
186
|
},
|
|
187
187
|
'Should throw validation error for invalid filter field'
|
|
188
|
-
)
|
|
189
|
-
})
|
|
190
|
-
})
|
|
188
|
+
)
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
191
|
|
|
192
192
|
describe('Sorting', () => {
|
|
193
193
|
beforeEach(async () => {
|
|
194
194
|
await cleanTables(knex, [
|
|
195
195
|
'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors'
|
|
196
|
-
])
|
|
196
|
+
])
|
|
197
197
|
|
|
198
198
|
// Create test data
|
|
199
|
-
const countryDoc = createJsonApiDocument('countries', { name: 'Test Country', code: 'TC' })
|
|
199
|
+
const countryDoc = createJsonApiDocument('countries', { name: 'Test Country', code: 'TC' })
|
|
200
200
|
const countryResult = await basicApi.resources.countries.post({
|
|
201
201
|
inputRecord: countryDoc,
|
|
202
202
|
simplified: false
|
|
203
|
-
})
|
|
203
|
+
})
|
|
204
204
|
|
|
205
205
|
// Create books with different titles
|
|
206
|
-
const titles = ['Zebra Book', 'Apple Book', 'Mango Book', 'Banana Book']
|
|
206
|
+
const titles = ['Zebra Book', 'Apple Book', 'Mango Book', 'Banana Book']
|
|
207
207
|
for (const title of titles) {
|
|
208
208
|
const doc = createJsonApiDocument('books', {
|
|
209
209
|
title
|
|
210
210
|
},
|
|
211
211
|
{
|
|
212
212
|
country: createRelationship(resourceIdentifier('countries', countryResult.data.id))
|
|
213
|
-
})
|
|
213
|
+
})
|
|
214
214
|
await basicApi.resources.books.post({
|
|
215
215
|
inputRecord: doc,
|
|
216
216
|
simplified: false
|
|
217
|
-
})
|
|
217
|
+
})
|
|
218
218
|
}
|
|
219
|
-
})
|
|
219
|
+
})
|
|
220
220
|
|
|
221
221
|
it('should sort by single field ascending', async () => {
|
|
222
222
|
const result = await basicApi.resources.books.query({
|
|
@@ -224,14 +224,14 @@ describe('Query Operations', () => {
|
|
|
224
224
|
sort: ['title']
|
|
225
225
|
},
|
|
226
226
|
simplified: false
|
|
227
|
-
})
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
validateJsonApiStructure(result, true)
|
|
230
|
+
assert.equal(result.data.length, 4)
|
|
228
231
|
|
|
229
|
-
|
|
230
|
-
assert.
|
|
231
|
-
|
|
232
|
-
const titles = result.data.map(b => b.attributes.title);
|
|
233
|
-
assert.deepEqual(titles, ['Apple Book', 'Banana Book', 'Mango Book', 'Zebra Book']);
|
|
234
|
-
});
|
|
232
|
+
const titles = result.data.map(b => b.attributes.title)
|
|
233
|
+
assert.deepEqual(titles, ['Apple Book', 'Banana Book', 'Mango Book', 'Zebra Book'])
|
|
234
|
+
})
|
|
235
235
|
|
|
236
236
|
it('should sort by single field descending', async () => {
|
|
237
237
|
const result = await basicApi.resources.books.query({
|
|
@@ -239,29 +239,29 @@ describe('Query Operations', () => {
|
|
|
239
239
|
sort: ['-title']
|
|
240
240
|
},
|
|
241
241
|
simplified: false
|
|
242
|
-
})
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
validateJsonApiStructure(result, true)
|
|
245
|
+
assert.equal(result.data.length, 4)
|
|
243
246
|
|
|
244
|
-
|
|
245
|
-
assert.
|
|
246
|
-
|
|
247
|
-
const titles = result.data.map(b => b.attributes.title);
|
|
248
|
-
assert.deepEqual(titles, ['Zebra Book', 'Mango Book', 'Banana Book', 'Apple Book']);
|
|
249
|
-
});
|
|
247
|
+
const titles = result.data.map(b => b.attributes.title)
|
|
248
|
+
assert.deepEqual(titles, ['Zebra Book', 'Mango Book', 'Banana Book', 'Apple Book'])
|
|
249
|
+
})
|
|
250
250
|
|
|
251
251
|
it('should sort by multiple fields', async () => {
|
|
252
252
|
// Create books with same country but different titles
|
|
253
|
-
const countryDoc1 = createJsonApiDocument('countries', { name: 'Country A', code: 'CA' })
|
|
254
|
-
const countryDoc2 = createJsonApiDocument('countries', { name: 'Country B', code: 'CB' })
|
|
255
|
-
|
|
253
|
+
const countryDoc1 = createJsonApiDocument('countries', { name: 'Country A', code: 'CA' })
|
|
254
|
+
const countryDoc2 = createJsonApiDocument('countries', { name: 'Country B', code: 'CB' })
|
|
255
|
+
|
|
256
256
|
const country1Result = await basicApi.resources.countries.post({
|
|
257
257
|
inputRecord: countryDoc1,
|
|
258
258
|
simplified: false
|
|
259
|
-
})
|
|
260
|
-
|
|
259
|
+
})
|
|
260
|
+
|
|
261
261
|
const country2Result = await basicApi.resources.countries.post({
|
|
262
262
|
inputRecord: countryDoc2,
|
|
263
263
|
simplified: false
|
|
264
|
-
})
|
|
264
|
+
})
|
|
265
265
|
|
|
266
266
|
// Create books
|
|
267
267
|
const booksToCreate = [
|
|
@@ -269,17 +269,17 @@ describe('Query Operations', () => {
|
|
|
269
269
|
{ title: 'Book A', countryId: country1Result.data.id },
|
|
270
270
|
{ title: 'Book Z', countryId: country2Result.data.id },
|
|
271
271
|
{ title: 'Book A', countryId: country2Result.data.id }
|
|
272
|
-
]
|
|
272
|
+
]
|
|
273
273
|
|
|
274
274
|
for (const bookData of booksToCreate) {
|
|
275
|
-
const doc = createJsonApiDocument('books',
|
|
275
|
+
const doc = createJsonApiDocument('books',
|
|
276
276
|
{ title: bookData.title },
|
|
277
277
|
{ country: createRelationship(resourceIdentifier('countries', bookData.countryId)) }
|
|
278
|
-
)
|
|
278
|
+
)
|
|
279
279
|
await basicApi.resources.books.post({
|
|
280
280
|
inputRecord: doc,
|
|
281
281
|
simplified: false
|
|
282
|
-
})
|
|
282
|
+
})
|
|
283
283
|
}
|
|
284
284
|
|
|
285
285
|
// Sort by country_id ascending, then title descending
|
|
@@ -288,68 +288,68 @@ describe('Query Operations', () => {
|
|
|
288
288
|
sort: ['country', '-title']
|
|
289
289
|
},
|
|
290
290
|
simplified: false
|
|
291
|
-
})
|
|
291
|
+
})
|
|
292
292
|
|
|
293
293
|
// We expect 8 books total (4 from beforeEach + 4 from this test)
|
|
294
|
-
assert.equal(result.data.length, 8)
|
|
295
|
-
|
|
294
|
+
assert.equal(result.data.length, 8)
|
|
295
|
+
|
|
296
296
|
// Filter to only the books we created in this test
|
|
297
|
-
const testBooks = result.data.filter(book =>
|
|
297
|
+
const testBooks = result.data.filter(book =>
|
|
298
298
|
book.attributes.title === 'Book A' || book.attributes.title === 'Book Z'
|
|
299
|
-
)
|
|
300
|
-
|
|
301
|
-
assert.equal(testBooks.length, 4)
|
|
302
|
-
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
assert.equal(testBooks.length, 4)
|
|
302
|
+
|
|
303
303
|
// Sort test books by country relationship for verification
|
|
304
304
|
const sortedTestBooks = testBooks.sort((a, b) => {
|
|
305
|
-
const countryA = a.relationships.country.data.id
|
|
306
|
-
const countryB = b.relationships.country.data.id
|
|
305
|
+
const countryA = a.relationships.country.data.id
|
|
306
|
+
const countryB = b.relationships.country.data.id
|
|
307
307
|
if (countryA !== countryB) {
|
|
308
|
-
return countryA.localeCompare(countryB)
|
|
308
|
+
return countryA.localeCompare(countryB)
|
|
309
309
|
}
|
|
310
310
|
// Within same country, sort by title descending
|
|
311
|
-
return b.attributes.title.localeCompare(a.attributes.title)
|
|
312
|
-
})
|
|
313
|
-
|
|
311
|
+
return b.attributes.title.localeCompare(a.attributes.title)
|
|
312
|
+
})
|
|
313
|
+
|
|
314
314
|
// Verify the books are from the correct countries and in correct order
|
|
315
|
-
assert.equal(sortedTestBooks[0].relationships.country.data.id, country1Result.data.id)
|
|
316
|
-
assert.equal(sortedTestBooks[1].relationships.country.data.id, country1Result.data.id)
|
|
317
|
-
assert.equal(sortedTestBooks[2].relationships.country.data.id, country2Result.data.id)
|
|
318
|
-
assert.equal(sortedTestBooks[3].relationships.country.data.id, country2Result.data.id)
|
|
319
|
-
|
|
315
|
+
assert.equal(sortedTestBooks[0].relationships.country.data.id, country1Result.data.id)
|
|
316
|
+
assert.equal(sortedTestBooks[1].relationships.country.data.id, country1Result.data.id)
|
|
317
|
+
assert.equal(sortedTestBooks[2].relationships.country.data.id, country2Result.data.id)
|
|
318
|
+
assert.equal(sortedTestBooks[3].relationships.country.data.id, country2Result.data.id)
|
|
319
|
+
|
|
320
320
|
// Within each country, should be sorted by title descending
|
|
321
|
-
assert.equal(sortedTestBooks[0].attributes.title, 'Book Z')
|
|
322
|
-
assert.equal(sortedTestBooks[1].attributes.title, 'Book A')
|
|
323
|
-
assert.equal(sortedTestBooks[2].attributes.title, 'Book Z')
|
|
324
|
-
assert.equal(sortedTestBooks[3].attributes.title, 'Book A')
|
|
325
|
-
})
|
|
326
|
-
})
|
|
321
|
+
assert.equal(sortedTestBooks[0].attributes.title, 'Book Z')
|
|
322
|
+
assert.equal(sortedTestBooks[1].attributes.title, 'Book A')
|
|
323
|
+
assert.equal(sortedTestBooks[2].attributes.title, 'Book Z')
|
|
324
|
+
assert.equal(sortedTestBooks[3].attributes.title, 'Book A')
|
|
325
|
+
})
|
|
326
|
+
})
|
|
327
327
|
|
|
328
328
|
describe('Pagination', () => {
|
|
329
329
|
beforeEach(async () => {
|
|
330
330
|
await cleanTables(knex, [
|
|
331
331
|
'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors'
|
|
332
|
-
])
|
|
332
|
+
])
|
|
333
333
|
|
|
334
334
|
// Create test data
|
|
335
|
-
const countryDoc = createJsonApiDocument('countries', { name: 'Test Country', code: 'TC' })
|
|
335
|
+
const countryDoc = createJsonApiDocument('countries', { name: 'Test Country', code: 'TC' })
|
|
336
336
|
const countryResult = await basicApi.resources.countries.post({
|
|
337
337
|
inputRecord: countryDoc,
|
|
338
338
|
simplified: false
|
|
339
|
-
})
|
|
339
|
+
})
|
|
340
340
|
|
|
341
341
|
// Create 10 books
|
|
342
342
|
for (let i = 1; i <= 10; i++) {
|
|
343
|
-
const doc = createJsonApiDocument('books',
|
|
343
|
+
const doc = createJsonApiDocument('books',
|
|
344
344
|
{ title: `Book ${String(i).padStart(2, '0')}` },
|
|
345
345
|
{ country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
|
|
346
|
-
)
|
|
346
|
+
)
|
|
347
347
|
await basicApi.resources.books.post({
|
|
348
348
|
inputRecord: doc,
|
|
349
349
|
simplified: false
|
|
350
|
-
})
|
|
350
|
+
})
|
|
351
351
|
}
|
|
352
|
-
})
|
|
352
|
+
})
|
|
353
353
|
|
|
354
354
|
it('should paginate results', async () => {
|
|
355
355
|
// Get first page
|
|
@@ -359,13 +359,13 @@ describe('Query Operations', () => {
|
|
|
359
359
|
sort: ['title']
|
|
360
360
|
},
|
|
361
361
|
simplified: false
|
|
362
|
-
})
|
|
362
|
+
})
|
|
363
363
|
|
|
364
|
-
validateJsonApiStructure(page1, true)
|
|
365
|
-
assert.equal(page1.data.length, 3, 'Should return 3 results for page 1')
|
|
366
|
-
|
|
367
|
-
const page1Titles = page1.data.map(b => b.attributes.title)
|
|
368
|
-
assert.deepEqual(page1Titles, ['Book 01', 'Book 02', 'Book 03'])
|
|
364
|
+
validateJsonApiStructure(page1, true)
|
|
365
|
+
assert.equal(page1.data.length, 3, 'Should return 3 results for page 1')
|
|
366
|
+
|
|
367
|
+
const page1Titles = page1.data.map(b => b.attributes.title)
|
|
368
|
+
assert.deepEqual(page1Titles, ['Book 01', 'Book 02', 'Book 03'])
|
|
369
369
|
|
|
370
370
|
// Get second page
|
|
371
371
|
const page2 = await basicApi.resources.books.query({
|
|
@@ -374,19 +374,19 @@ describe('Query Operations', () => {
|
|
|
374
374
|
sort: ['title']
|
|
375
375
|
},
|
|
376
376
|
simplified: false
|
|
377
|
-
})
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
validateJsonApiStructure(page2, true)
|
|
380
|
+
assert.equal(page2.data.length, 3, 'Should return 3 results for page 2')
|
|
378
381
|
|
|
379
|
-
|
|
380
|
-
assert.
|
|
381
|
-
|
|
382
|
-
const page2Titles = page2.data.map(b => b.attributes.title);
|
|
383
|
-
assert.deepEqual(page2Titles, ['Book 04', 'Book 05', 'Book 06']);
|
|
382
|
+
const page2Titles = page2.data.map(b => b.attributes.title)
|
|
383
|
+
assert.deepEqual(page2Titles, ['Book 04', 'Book 05', 'Book 06'])
|
|
384
384
|
|
|
385
385
|
// Verify different results
|
|
386
|
-
const page1Ids = page1.data.map(b => b.id)
|
|
387
|
-
const page2Ids = page2.data.map(b => b.id)
|
|
388
|
-
assert.equal(page1Ids.some(id => page2Ids.includes(id)), false, 'Pages should have different results')
|
|
389
|
-
})
|
|
386
|
+
const page1Ids = page1.data.map(b => b.id)
|
|
387
|
+
const page2Ids = page2.data.map(b => b.id)
|
|
388
|
+
assert.equal(page1Ids.some(id => page2Ids.includes(id)), false, 'Pages should have different results')
|
|
389
|
+
})
|
|
390
390
|
|
|
391
391
|
it('should handle last page with fewer results', async () => {
|
|
392
392
|
const lastPage = await basicApi.resources.books.query({
|
|
@@ -395,12 +395,12 @@ describe('Query Operations', () => {
|
|
|
395
395
|
sort: ['title']
|
|
396
396
|
},
|
|
397
397
|
simplified: false
|
|
398
|
-
})
|
|
398
|
+
})
|
|
399
399
|
|
|
400
|
-
validateJsonApiStructure(lastPage, true)
|
|
401
|
-
assert.equal(lastPage.data.length, 1, 'Should return 1 result for last page')
|
|
402
|
-
assert.equal(lastPage.data[0].attributes.title, 'Book 10')
|
|
403
|
-
})
|
|
400
|
+
validateJsonApiStructure(lastPage, true)
|
|
401
|
+
assert.equal(lastPage.data.length, 1, 'Should return 1 result for last page')
|
|
402
|
+
assert.equal(lastPage.data[0].attributes.title, 'Book 10')
|
|
403
|
+
})
|
|
404
404
|
|
|
405
405
|
it('should handle page beyond available data', async () => {
|
|
406
406
|
const emptyPage = await basicApi.resources.books.query({
|
|
@@ -408,11 +408,11 @@ describe('Query Operations', () => {
|
|
|
408
408
|
page: { number: 10, size: 5 }
|
|
409
409
|
},
|
|
410
410
|
simplified: false
|
|
411
|
-
})
|
|
411
|
+
})
|
|
412
412
|
|
|
413
|
-
validateJsonApiStructure(emptyPage, true)
|
|
414
|
-
assert.equal(emptyPage.data.length, 0, 'Should return empty array for page beyond data')
|
|
415
|
-
})
|
|
413
|
+
validateJsonApiStructure(emptyPage, true)
|
|
414
|
+
assert.equal(emptyPage.data.length, 0, 'Should return empty array for page beyond data')
|
|
415
|
+
})
|
|
416
416
|
|
|
417
417
|
it('should respect queryMaxLimit limit', async () => {
|
|
418
418
|
// Assuming queryMaxLimit is 100 (from vars)
|
|
@@ -421,34 +421,34 @@ describe('Query Operations', () => {
|
|
|
421
421
|
page: { number: 1, size: 200 } // Request more than max
|
|
422
422
|
},
|
|
423
423
|
simplified: false
|
|
424
|
-
})
|
|
424
|
+
})
|
|
425
425
|
|
|
426
|
-
validateJsonApiStructure(result, true)
|
|
427
|
-
assert(result.data.length <= 100, 'Should not exceed queryMaxLimit')
|
|
428
|
-
})
|
|
429
|
-
})
|
|
426
|
+
validateJsonApiStructure(result, true)
|
|
427
|
+
assert(result.data.length <= 100, 'Should not exceed queryMaxLimit')
|
|
428
|
+
})
|
|
429
|
+
})
|
|
430
430
|
|
|
431
431
|
describe('Combined Query Features', () => {
|
|
432
432
|
beforeEach(async () => {
|
|
433
433
|
await cleanTables(knex, [
|
|
434
434
|
'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors',
|
|
435
435
|
'ext_countries', 'ext_publishers', 'ext_authors', 'ext_books', 'ext_book_authors'
|
|
436
|
-
])
|
|
436
|
+
])
|
|
437
437
|
|
|
438
438
|
// Create test data for extended API with more fields
|
|
439
|
-
const countries = []
|
|
439
|
+
const countries = []
|
|
440
440
|
for (const [name, code, pop] of [['USA', 'US', 300000000], ['UK', 'GB', 60000000], ['France', 'FR', 65000000]]) {
|
|
441
441
|
const doc = createJsonApiDocument('countries', {
|
|
442
442
|
name,
|
|
443
443
|
code,
|
|
444
444
|
population: pop,
|
|
445
445
|
currency: code === 'US' ? 'USD' : code === 'GB' ? 'GBP' : 'EUR'
|
|
446
|
-
})
|
|
446
|
+
})
|
|
447
447
|
const result = await extendedApi.resources.countries.post({
|
|
448
448
|
inputRecord: doc,
|
|
449
449
|
simplified: false
|
|
450
|
-
})
|
|
451
|
-
countries.push(result.data)
|
|
450
|
+
})
|
|
451
|
+
countries.push(result.data)
|
|
452
452
|
}
|
|
453
453
|
|
|
454
454
|
// Create books with prices
|
|
@@ -458,23 +458,23 @@ describe('Query Operations', () => {
|
|
|
458
458
|
{ title: 'Medium Book C', price: 19.99, countryId: countries[1].id, language: 'en' },
|
|
459
459
|
{ title: 'Cheap Book D', price: 12.99, countryId: countries[1].id, language: 'fr' },
|
|
460
460
|
{ title: 'Expensive Book E', price: 39.99, countryId: countries[2].id, language: 'fr' }
|
|
461
|
-
]
|
|
461
|
+
]
|
|
462
462
|
|
|
463
463
|
for (const data of bookData) {
|
|
464
|
-
const doc = createJsonApiDocument('books',
|
|
464
|
+
const doc = createJsonApiDocument('books',
|
|
465
465
|
{
|
|
466
466
|
title: data.title,
|
|
467
467
|
price: data.price,
|
|
468
468
|
language: data.language
|
|
469
469
|
},
|
|
470
470
|
{ country: createRelationship(resourceIdentifier('countries', data.countryId)) }
|
|
471
|
-
)
|
|
471
|
+
)
|
|
472
472
|
await extendedApi.resources.books.post({
|
|
473
473
|
inputRecord: doc,
|
|
474
474
|
simplified: false
|
|
475
|
-
})
|
|
475
|
+
})
|
|
476
476
|
}
|
|
477
|
-
})
|
|
477
|
+
})
|
|
478
478
|
|
|
479
479
|
it('should filter, sort, and paginate together', async () => {
|
|
480
480
|
const result = await extendedApi.resources.books.query({
|
|
@@ -484,15 +484,15 @@ describe('Query Operations', () => {
|
|
|
484
484
|
page: { number: 1, size: 2 }
|
|
485
485
|
},
|
|
486
486
|
simplified: false
|
|
487
|
-
})
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
validateJsonApiStructure(result, true)
|
|
490
|
+
assert.equal(result.data.length, 2)
|
|
488
491
|
|
|
489
|
-
validateJsonApiStructure(result, true);
|
|
490
|
-
assert.equal(result.data.length, 2);
|
|
491
|
-
|
|
492
492
|
// Should get the 2 most expensive English books
|
|
493
|
-
assert.equal(result.data[0].attributes.title, 'Expensive Book B')
|
|
494
|
-
assert.equal(result.data[1].attributes.title, 'Medium Book C')
|
|
495
|
-
})
|
|
493
|
+
assert.equal(result.data[0].attributes.title, 'Expensive Book B')
|
|
494
|
+
assert.equal(result.data[1].attributes.title, 'Medium Book C')
|
|
495
|
+
})
|
|
496
496
|
|
|
497
497
|
it('should filter and include relationships', async () => {
|
|
498
498
|
const result = await extendedApi.resources.books.query({
|
|
@@ -501,21 +501,21 @@ describe('Query Operations', () => {
|
|
|
501
501
|
include: ['country']
|
|
502
502
|
},
|
|
503
503
|
simplified: false
|
|
504
|
-
})
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
validateJsonApiStructure(result, true)
|
|
507
|
+
assert.equal(result.data.length, 2, 'Should return 2 French books')
|
|
505
508
|
|
|
506
|
-
validateJsonApiStructure(result, true);
|
|
507
|
-
assert.equal(result.data.length, 2, 'Should return 2 French books');
|
|
508
|
-
|
|
509
509
|
// Verify includes
|
|
510
|
-
assert(result.included, 'Should have included data')
|
|
511
|
-
const includedCountries = result.included.filter(r => r.type === 'countries')
|
|
512
|
-
assert(includedCountries.length >= 1, 'Should include at least one country')
|
|
513
|
-
|
|
510
|
+
assert(result.included, 'Should have included data')
|
|
511
|
+
const includedCountries = result.included.filter(r => r.type === 'countries')
|
|
512
|
+
assert(includedCountries.length >= 1, 'Should include at least one country')
|
|
513
|
+
|
|
514
514
|
// Verify included countries have the expected fields
|
|
515
515
|
for (const country of includedCountries) {
|
|
516
|
-
assert(country.attributes.name)
|
|
517
|
-
assert(country.attributes.currency)
|
|
516
|
+
assert(country.attributes.name)
|
|
517
|
+
assert(country.attributes.currency)
|
|
518
518
|
}
|
|
519
|
-
})
|
|
520
|
-
})
|
|
521
|
-
})
|
|
519
|
+
})
|
|
520
|
+
})
|
|
521
|
+
})
|