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,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 { createBulkOperationsApi } 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 { createBulkOperationsApi } from './fixtures/api-configs.js'
|
|
5
5
|
import {
|
|
6
6
|
validateJsonApiStructure,
|
|
7
7
|
resourceIdentifier,
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
createJsonApiDocument,
|
|
10
10
|
createRelationship,
|
|
11
11
|
createToManyRelationship
|
|
12
|
-
} from './helpers/test-utils.js'
|
|
12
|
+
} from './helpers/test-utils.js'
|
|
13
13
|
|
|
14
14
|
// Create Knex instance for tests
|
|
15
15
|
const knex = knexLib({
|
|
@@ -18,87 +18,87 @@ const knex = knexLib({
|
|
|
18
18
|
filename: ':memory:'
|
|
19
19
|
},
|
|
20
20
|
useNullAsDefault: true
|
|
21
|
-
})
|
|
21
|
+
})
|
|
22
22
|
|
|
23
23
|
// API instance that persists across tests
|
|
24
|
-
let api
|
|
24
|
+
let api
|
|
25
25
|
|
|
26
26
|
describe('Bulk Operations', () => {
|
|
27
27
|
before(async () => {
|
|
28
28
|
// Initialize API once
|
|
29
|
-
api = await createBulkOperationsApi(knex)
|
|
30
|
-
})
|
|
29
|
+
api = await createBulkOperationsApi(knex)
|
|
30
|
+
})
|
|
31
31
|
|
|
32
32
|
after(async () => {
|
|
33
33
|
// Close database connection to allow tests to exit
|
|
34
|
-
await knex.destroy()
|
|
35
|
-
})
|
|
34
|
+
await knex.destroy()
|
|
35
|
+
})
|
|
36
36
|
|
|
37
37
|
describe('Bulk Create (bulkPost)', () => {
|
|
38
38
|
beforeEach(async () => {
|
|
39
39
|
// Clean all tables before each test
|
|
40
40
|
await cleanTables(knex, [
|
|
41
41
|
'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors'
|
|
42
|
-
])
|
|
43
|
-
})
|
|
42
|
+
])
|
|
43
|
+
})
|
|
44
44
|
|
|
45
45
|
it('should create multiple records atomically', async () => {
|
|
46
46
|
const records = [
|
|
47
47
|
createJsonApiDocument('authors', { name: 'Author One' }),
|
|
48
48
|
createJsonApiDocument('authors', { name: 'Author Two' }),
|
|
49
49
|
createJsonApiDocument('authors', { name: 'Author Three' })
|
|
50
|
-
]
|
|
50
|
+
]
|
|
51
51
|
|
|
52
52
|
const result = await api.scopes.authors.bulkPost({
|
|
53
53
|
inputRecords: records,
|
|
54
54
|
atomic: true
|
|
55
|
-
})
|
|
55
|
+
})
|
|
56
56
|
|
|
57
57
|
// Validate response structure
|
|
58
|
-
assert(result.data, 'Should have data array')
|
|
59
|
-
assert(Array.isArray(result.data), 'Data should be an array')
|
|
60
|
-
assert.equal(result.data.length, 3, 'Should return 3 created records')
|
|
58
|
+
assert(result.data, 'Should have data array')
|
|
59
|
+
assert(Array.isArray(result.data), 'Data should be an array')
|
|
60
|
+
assert.equal(result.data.length, 3, 'Should return 3 created records')
|
|
61
61
|
|
|
62
62
|
// Validate meta
|
|
63
|
-
assert.equal(result.meta.total, 3)
|
|
64
|
-
assert.equal(result.meta.succeeded, 3)
|
|
65
|
-
assert.equal(result.meta.failed, 0)
|
|
66
|
-
assert.equal(result.meta.atomic, true)
|
|
63
|
+
assert.equal(result.meta.total, 3)
|
|
64
|
+
assert.equal(result.meta.succeeded, 3)
|
|
65
|
+
assert.equal(result.meta.failed, 0)
|
|
66
|
+
assert.equal(result.meta.atomic, true)
|
|
67
67
|
|
|
68
68
|
// Validate each created record
|
|
69
69
|
result.data.forEach((record, index) => {
|
|
70
|
-
validateJsonApiStructure({ data: record }, false)
|
|
71
|
-
assert.equal(record.type, 'authors')
|
|
72
|
-
assert(record.id, 'Should have generated ID')
|
|
73
|
-
assert.equal(record.attributes.name, `Author ${['One', 'Two', 'Three'][index]}`)
|
|
74
|
-
})
|
|
70
|
+
validateJsonApiStructure({ data: record }, false)
|
|
71
|
+
assert.equal(record.type, 'authors')
|
|
72
|
+
assert(record.id, 'Should have generated ID')
|
|
73
|
+
assert.equal(record.attributes.name, `Author ${['One', 'Two', 'Three'][index]}`)
|
|
74
|
+
})
|
|
75
75
|
|
|
76
76
|
// Verify via API query
|
|
77
|
-
const queryResult = await api.resources.authors.query({ simplified: false })
|
|
78
|
-
assert.equal(queryResult.data.length, 3, 'Should have 3 records via query')
|
|
79
|
-
})
|
|
77
|
+
const queryResult = await api.resources.authors.query({ simplified: false })
|
|
78
|
+
assert.equal(queryResult.data.length, 3, 'Should have 3 records via query')
|
|
79
|
+
})
|
|
80
80
|
|
|
81
81
|
it('should rollback all records on error in atomic mode', async () => {
|
|
82
82
|
const records = [
|
|
83
83
|
createJsonApiDocument('authors', { name: 'Valid Author' }),
|
|
84
84
|
createJsonApiDocument('authors', {}), // Invalid - missing required name
|
|
85
85
|
createJsonApiDocument('authors', { name: 'Another Valid' })
|
|
86
|
-
]
|
|
86
|
+
]
|
|
87
87
|
|
|
88
88
|
try {
|
|
89
89
|
await api.scopes.authors.bulkPost({
|
|
90
90
|
inputRecords: records,
|
|
91
91
|
atomic: true
|
|
92
|
-
})
|
|
93
|
-
assert.fail('Should have thrown error')
|
|
92
|
+
})
|
|
93
|
+
assert.fail('Should have thrown error')
|
|
94
94
|
} catch (error) {
|
|
95
|
-
assert(error.message.includes('validation failed') || error.message.includes('required'), 'Error should mention validation or required field')
|
|
95
|
+
assert(error.message.includes('validation failed') || error.message.includes('required'), 'Error should mention validation or required field')
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
// Verify no records were created via API
|
|
99
|
-
const queryResult = await api.resources.authors.query({ simplified: false })
|
|
100
|
-
assert.equal(queryResult.data.length, 0, 'Should have no records due to rollback')
|
|
101
|
-
})
|
|
99
|
+
const queryResult = await api.resources.authors.query({ simplified: false })
|
|
100
|
+
assert.equal(queryResult.data.length, 0, 'Should have no records due to rollback')
|
|
101
|
+
})
|
|
102
102
|
|
|
103
103
|
it('should allow partial success in non-atomic mode', async () => {
|
|
104
104
|
const records = [
|
|
@@ -106,118 +106,118 @@ describe('Bulk Operations', () => {
|
|
|
106
106
|
createJsonApiDocument('authors', {}), // Invalid
|
|
107
107
|
createJsonApiDocument('authors', { name: 'Valid Author 2' }),
|
|
108
108
|
createJsonApiDocument('authors', { name: 'Valid Author 3' })
|
|
109
|
-
]
|
|
109
|
+
]
|
|
110
110
|
|
|
111
111
|
const result = await api.scopes.authors.bulkPost({
|
|
112
112
|
inputRecords: records,
|
|
113
113
|
atomic: false
|
|
114
|
-
})
|
|
114
|
+
})
|
|
115
115
|
|
|
116
116
|
// Check succeeded records
|
|
117
|
-
assert.equal(result.data.length, 3, 'Should have 3 successful records')
|
|
118
|
-
assert.equal(result.meta.succeeded, 3)
|
|
119
|
-
assert.equal(result.meta.failed, 1)
|
|
117
|
+
assert.equal(result.data.length, 3, 'Should have 3 successful records')
|
|
118
|
+
assert.equal(result.meta.succeeded, 3)
|
|
119
|
+
assert.equal(result.meta.failed, 1)
|
|
120
120
|
|
|
121
121
|
// Check errors
|
|
122
|
-
assert(result.errors, 'Should have errors array')
|
|
123
|
-
assert.equal(result.errors.length, 1)
|
|
124
|
-
|
|
122
|
+
assert(result.errors, 'Should have errors array')
|
|
123
|
+
assert.equal(result.errors.length, 1)
|
|
124
|
+
|
|
125
125
|
// Error should be for missing name
|
|
126
|
-
assert.equal(result.errors[0].index, 1)
|
|
127
|
-
assert(result.errors[0].error.message.includes('validation failed') || result.errors[0].error.message.includes('required'))
|
|
126
|
+
assert.equal(result.errors[0].index, 1)
|
|
127
|
+
assert(result.errors[0].error.message.includes('validation failed') || result.errors[0].error.message.includes('required'))
|
|
128
128
|
|
|
129
129
|
// Verify via API
|
|
130
|
-
const queryResult = await api.resources.authors.query({ simplified: false })
|
|
131
|
-
assert.equal(queryResult.data.length, 3)
|
|
132
|
-
})
|
|
130
|
+
const queryResult = await api.resources.authors.query({ simplified: false })
|
|
131
|
+
assert.equal(queryResult.data.length, 3)
|
|
132
|
+
})
|
|
133
133
|
|
|
134
134
|
it('should create records with relationships', async () => {
|
|
135
135
|
// First create a country
|
|
136
136
|
const countryResult = await api.resources.countries.post({
|
|
137
137
|
inputRecord: createJsonApiDocument('countries', { name: 'Test Country', code: 'TC' }),
|
|
138
138
|
simplified: false
|
|
139
|
-
})
|
|
139
|
+
})
|
|
140
140
|
|
|
141
141
|
const records = [
|
|
142
|
-
createJsonApiDocument('publishers',
|
|
142
|
+
createJsonApiDocument('publishers',
|
|
143
143
|
{ name: 'Publisher One' },
|
|
144
144
|
{ country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
|
|
145
145
|
),
|
|
146
|
-
createJsonApiDocument('publishers',
|
|
146
|
+
createJsonApiDocument('publishers',
|
|
147
147
|
{ name: 'Publisher Two' },
|
|
148
148
|
{ country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
|
|
149
149
|
)
|
|
150
|
-
]
|
|
150
|
+
]
|
|
151
151
|
|
|
152
152
|
const result = await api.scopes.publishers.bulkPost({
|
|
153
153
|
inputRecords: records
|
|
154
|
-
})
|
|
154
|
+
})
|
|
155
155
|
|
|
156
|
-
assert.equal(result.data.length, 2)
|
|
156
|
+
assert.equal(result.data.length, 2)
|
|
157
157
|
result.data.forEach(publisher => {
|
|
158
|
-
assert.equal(publisher.relationships.country.data.id, countryResult.data.id)
|
|
159
|
-
})
|
|
160
|
-
})
|
|
158
|
+
assert.equal(publisher.relationships.country.data.id, countryResult.data.id)
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
161
|
|
|
162
162
|
it('should enforce max bulk operations limit', async () => {
|
|
163
|
-
const tooManyRecords = Array.from({ length: 101 }, (_, i) =>
|
|
163
|
+
const tooManyRecords = Array.from({ length: 101 }, (_, i) =>
|
|
164
164
|
createJsonApiDocument('authors', { name: `Author ${i}` })
|
|
165
|
-
)
|
|
165
|
+
)
|
|
166
166
|
|
|
167
167
|
try {
|
|
168
168
|
await api.scopes.authors.bulkPost({
|
|
169
169
|
inputRecords: tooManyRecords
|
|
170
|
-
})
|
|
171
|
-
assert.fail('Should have thrown error')
|
|
170
|
+
})
|
|
171
|
+
assert.fail('Should have thrown error')
|
|
172
172
|
} catch (error) {
|
|
173
|
-
assert(error.message.includes('limited to 100'))
|
|
173
|
+
assert(error.message.includes('limited to 100'))
|
|
174
174
|
}
|
|
175
|
-
})
|
|
175
|
+
})
|
|
176
176
|
|
|
177
177
|
it('should respect atomic query parameter override', async () => {
|
|
178
178
|
const records = [
|
|
179
179
|
createJsonApiDocument('authors', { name: 'Valid' }),
|
|
180
180
|
createJsonApiDocument('authors', {}) // Invalid
|
|
181
|
-
]
|
|
181
|
+
]
|
|
182
182
|
|
|
183
183
|
// Override default atomic mode
|
|
184
184
|
const result = await api.scopes.authors.bulkPost({
|
|
185
185
|
inputRecords: records,
|
|
186
186
|
atomic: false // Override to non-atomic
|
|
187
|
-
})
|
|
187
|
+
})
|
|
188
188
|
|
|
189
|
-
assert.equal(result.meta.succeeded, 1)
|
|
190
|
-
assert.equal(result.meta.failed, 1)
|
|
191
|
-
assert.equal(result.meta.atomic, false)
|
|
192
|
-
})
|
|
193
|
-
})
|
|
189
|
+
assert.equal(result.meta.succeeded, 1)
|
|
190
|
+
assert.equal(result.meta.failed, 1)
|
|
191
|
+
assert.equal(result.meta.atomic, false)
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
194
|
|
|
195
195
|
describe('Bulk Update (bulkPatch)', () => {
|
|
196
|
-
|
|
196
|
+
const testData = {}
|
|
197
197
|
|
|
198
198
|
beforeEach(async () => {
|
|
199
199
|
await cleanTables(knex, [
|
|
200
200
|
'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors'
|
|
201
|
-
])
|
|
201
|
+
])
|
|
202
202
|
|
|
203
203
|
// Create test data via API
|
|
204
204
|
const authors = await Promise.all([
|
|
205
|
-
api.resources.authors.post({
|
|
205
|
+
api.resources.authors.post({
|
|
206
206
|
inputRecord: createJsonApiDocument('authors', { name: 'Author One' }),
|
|
207
207
|
simplified: false
|
|
208
208
|
}),
|
|
209
|
-
api.resources.authors.post({
|
|
209
|
+
api.resources.authors.post({
|
|
210
210
|
inputRecord: createJsonApiDocument('authors', { name: 'Author Two' }),
|
|
211
211
|
simplified: false
|
|
212
212
|
}),
|
|
213
|
-
api.resources.authors.post({
|
|
213
|
+
api.resources.authors.post({
|
|
214
214
|
inputRecord: createJsonApiDocument('authors', { name: 'Author Three' }),
|
|
215
215
|
simplified: false
|
|
216
216
|
})
|
|
217
|
-
])
|
|
217
|
+
])
|
|
218
218
|
|
|
219
|
-
testData.authorIds = authors.map(a => a.data.id)
|
|
220
|
-
})
|
|
219
|
+
testData.authorIds = authors.map(a => a.data.id)
|
|
220
|
+
})
|
|
221
221
|
|
|
222
222
|
it('should update multiple records atomically', async () => {
|
|
223
223
|
const operations = [
|
|
@@ -237,28 +237,28 @@ describe('Bulk Operations', () => {
|
|
|
237
237
|
attributes: { name: 'Updated Author Two' }
|
|
238
238
|
}
|
|
239
239
|
}
|
|
240
|
-
]
|
|
240
|
+
]
|
|
241
241
|
|
|
242
242
|
const result = await api.scopes.authors.bulkPatch({
|
|
243
243
|
operations,
|
|
244
244
|
atomic: true
|
|
245
|
-
})
|
|
245
|
+
})
|
|
246
246
|
|
|
247
|
-
assert.equal(result.data.length, 2)
|
|
248
|
-
assert.equal(result.meta.succeeded, 2)
|
|
249
|
-
assert.equal(result.meta.failed, 0)
|
|
247
|
+
assert.equal(result.data.length, 2)
|
|
248
|
+
assert.equal(result.meta.succeeded, 2)
|
|
249
|
+
assert.equal(result.meta.failed, 0)
|
|
250
250
|
|
|
251
251
|
// Verify updates in response
|
|
252
|
-
assert.equal(result.data[0].attributes.name, 'Updated Author One')
|
|
253
|
-
assert.equal(result.data[1].attributes.name, 'Updated Author Two')
|
|
252
|
+
assert.equal(result.data[0].attributes.name, 'Updated Author One')
|
|
253
|
+
assert.equal(result.data[1].attributes.name, 'Updated Author Two')
|
|
254
254
|
|
|
255
255
|
// Verify via API
|
|
256
|
-
const author1 = await api.resources.authors.get({
|
|
256
|
+
const author1 = await api.resources.authors.get({
|
|
257
257
|
id: testData.authorIds[0],
|
|
258
258
|
simplified: false
|
|
259
|
-
})
|
|
260
|
-
assert.equal(author1.data.attributes.name, 'Updated Author One')
|
|
261
|
-
})
|
|
259
|
+
})
|
|
260
|
+
assert.equal(author1.data.attributes.name, 'Updated Author One')
|
|
261
|
+
})
|
|
262
262
|
|
|
263
263
|
it('should handle non-existent IDs in non-atomic mode', async () => {
|
|
264
264
|
const operations = [
|
|
@@ -286,18 +286,18 @@ describe('Bulk Operations', () => {
|
|
|
286
286
|
attributes: { name: 'Also Updated' }
|
|
287
287
|
}
|
|
288
288
|
}
|
|
289
|
-
]
|
|
289
|
+
]
|
|
290
290
|
|
|
291
291
|
const result = await api.scopes.authors.bulkPatch({
|
|
292
292
|
operations,
|
|
293
293
|
atomic: false
|
|
294
|
-
})
|
|
294
|
+
})
|
|
295
295
|
|
|
296
|
-
assert.equal(result.meta.succeeded, 2)
|
|
297
|
-
assert.equal(result.meta.failed, 1)
|
|
298
|
-
assert.equal(result.errors.length, 1)
|
|
299
|
-
assert.equal(result.errors[0].id, '999999')
|
|
300
|
-
})
|
|
296
|
+
assert.equal(result.meta.succeeded, 2)
|
|
297
|
+
assert.equal(result.meta.failed, 1)
|
|
298
|
+
assert.equal(result.errors.length, 1)
|
|
299
|
+
assert.equal(result.errors[0].id, '999999')
|
|
300
|
+
})
|
|
301
301
|
|
|
302
302
|
it('should validate operation structure', async () => {
|
|
303
303
|
const invalidOperations = [
|
|
@@ -311,25 +311,25 @@ describe('Bulk Operations', () => {
|
|
|
311
311
|
attributes: { name: 'Valid' }
|
|
312
312
|
}
|
|
313
313
|
}
|
|
314
|
-
]
|
|
314
|
+
]
|
|
315
315
|
|
|
316
316
|
const result = await api.scopes.authors.bulkPatch({
|
|
317
317
|
operations: invalidOperations,
|
|
318
318
|
atomic: false
|
|
319
|
-
})
|
|
319
|
+
})
|
|
320
320
|
|
|
321
|
-
assert.equal(result.meta.succeeded, 1)
|
|
322
|
-
assert.equal(result.meta.failed, 2)
|
|
323
|
-
assert.equal(result.errors[0].error.code, 'INVALID_OPERATION')
|
|
324
|
-
assert.equal(result.errors[1].error.code, 'INVALID_OPERATION')
|
|
325
|
-
})
|
|
321
|
+
assert.equal(result.meta.succeeded, 1)
|
|
322
|
+
assert.equal(result.meta.failed, 2)
|
|
323
|
+
assert.equal(result.errors[0].error.code, 'INVALID_OPERATION')
|
|
324
|
+
assert.equal(result.errors[1].error.code, 'INVALID_OPERATION')
|
|
325
|
+
})
|
|
326
326
|
|
|
327
327
|
it('should rollback all updates on error in atomic mode', async () => {
|
|
328
328
|
// Get original name via API
|
|
329
|
-
const originalAuthor = await api.resources.authors.get({
|
|
329
|
+
const originalAuthor = await api.resources.authors.get({
|
|
330
330
|
id: testData.authorIds[0],
|
|
331
331
|
simplified: false
|
|
332
|
-
})
|
|
332
|
+
})
|
|
333
333
|
|
|
334
334
|
const operations = [
|
|
335
335
|
{
|
|
@@ -348,71 +348,71 @@ describe('Bulk Operations', () => {
|
|
|
348
348
|
attributes: { name: 'Non-existent' }
|
|
349
349
|
}
|
|
350
350
|
}
|
|
351
|
-
]
|
|
351
|
+
]
|
|
352
352
|
|
|
353
353
|
try {
|
|
354
354
|
await api.scopes.authors.bulkPatch({
|
|
355
355
|
operations,
|
|
356
356
|
atomic: true
|
|
357
|
-
})
|
|
358
|
-
assert.fail('Should have thrown error')
|
|
357
|
+
})
|
|
358
|
+
assert.fail('Should have thrown error')
|
|
359
359
|
} catch (error) {
|
|
360
360
|
// Expected error
|
|
361
361
|
}
|
|
362
362
|
|
|
363
363
|
// Verify no changes were made via API
|
|
364
|
-
const author = await api.resources.authors.get({
|
|
364
|
+
const author = await api.resources.authors.get({
|
|
365
365
|
id: testData.authorIds[0],
|
|
366
366
|
simplified: false
|
|
367
|
-
})
|
|
368
|
-
assert.equal(author.data.attributes.name, originalAuthor.data.attributes.name, 'Name should not have changed')
|
|
369
|
-
})
|
|
370
|
-
})
|
|
367
|
+
})
|
|
368
|
+
assert.equal(author.data.attributes.name, originalAuthor.data.attributes.name, 'Name should not have changed')
|
|
369
|
+
})
|
|
370
|
+
})
|
|
371
371
|
|
|
372
372
|
describe('Bulk Delete (bulkDelete)', () => {
|
|
373
|
-
|
|
373
|
+
const testData = {}
|
|
374
374
|
|
|
375
375
|
beforeEach(async () => {
|
|
376
376
|
await cleanTables(knex, [
|
|
377
377
|
'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors'
|
|
378
|
-
])
|
|
378
|
+
])
|
|
379
379
|
|
|
380
380
|
// Create test data via API
|
|
381
381
|
const authors = await Promise.all(
|
|
382
|
-
Array.from({ length: 5 }, (_, i) =>
|
|
383
|
-
api.resources.authors.post({
|
|
382
|
+
Array.from({ length: 5 }, (_, i) =>
|
|
383
|
+
api.resources.authors.post({
|
|
384
384
|
inputRecord: createJsonApiDocument('authors', { name: `Author ${i + 1}` }),
|
|
385
385
|
simplified: false
|
|
386
386
|
})
|
|
387
387
|
)
|
|
388
|
-
)
|
|
388
|
+
)
|
|
389
389
|
|
|
390
|
-
testData.authorIds = authors.map(a => a.data.id)
|
|
391
|
-
})
|
|
390
|
+
testData.authorIds = authors.map(a => a.data.id)
|
|
391
|
+
})
|
|
392
392
|
|
|
393
393
|
it('should delete multiple records atomically', async () => {
|
|
394
|
-
const idsToDelete = [testData.authorIds[0], testData.authorIds[2], testData.authorIds[4]]
|
|
394
|
+
const idsToDelete = [testData.authorIds[0], testData.authorIds[2], testData.authorIds[4]]
|
|
395
395
|
|
|
396
396
|
const result = await api.scopes.authors.bulkDelete({
|
|
397
397
|
ids: idsToDelete,
|
|
398
398
|
atomic: true
|
|
399
|
-
})
|
|
399
|
+
})
|
|
400
400
|
|
|
401
|
-
assert.equal(result.meta.total, 3)
|
|
402
|
-
assert.equal(result.meta.succeeded, 3)
|
|
403
|
-
assert.equal(result.meta.failed, 0)
|
|
404
|
-
assert.deepEqual(result.meta.deleted, idsToDelete)
|
|
401
|
+
assert.equal(result.meta.total, 3)
|
|
402
|
+
assert.equal(result.meta.succeeded, 3)
|
|
403
|
+
assert.equal(result.meta.failed, 0)
|
|
404
|
+
assert.deepEqual(result.meta.deleted, idsToDelete)
|
|
405
405
|
|
|
406
406
|
// Verify via API query
|
|
407
|
-
const queryResult = await api.resources.authors.query({ simplified: false })
|
|
408
|
-
assert.equal(queryResult.data.length, 2, 'Should have 2 remaining records')
|
|
407
|
+
const queryResult = await api.resources.authors.query({ simplified: false })
|
|
408
|
+
assert.equal(queryResult.data.length, 2, 'Should have 2 remaining records')
|
|
409
409
|
|
|
410
410
|
// Verify specific records were deleted
|
|
411
|
-
const remainingIds = queryResult.data.map(r => r.id)
|
|
412
|
-
assert(!remainingIds.includes(testData.authorIds[0]))
|
|
413
|
-
assert(remainingIds.includes(testData.authorIds[1]))
|
|
414
|
-
assert(!remainingIds.includes(testData.authorIds[2]))
|
|
415
|
-
})
|
|
411
|
+
const remainingIds = queryResult.data.map(r => r.id)
|
|
412
|
+
assert(!remainingIds.includes(testData.authorIds[0]))
|
|
413
|
+
assert(remainingIds.includes(testData.authorIds[1]))
|
|
414
|
+
assert(!remainingIds.includes(testData.authorIds[2]))
|
|
415
|
+
})
|
|
416
416
|
|
|
417
417
|
it('should handle mixed valid/invalid IDs in non-atomic mode', async () => {
|
|
418
418
|
const idsToDelete = [
|
|
@@ -421,66 +421,66 @@ describe('Bulk Operations', () => {
|
|
|
421
421
|
testData.authorIds[2],
|
|
422
422
|
'888888', // Non-existent
|
|
423
423
|
testData.authorIds[4]
|
|
424
|
-
]
|
|
424
|
+
]
|
|
425
425
|
|
|
426
426
|
const result = await api.scopes.authors.bulkDelete({
|
|
427
427
|
ids: idsToDelete,
|
|
428
428
|
atomic: false
|
|
429
|
-
})
|
|
429
|
+
})
|
|
430
430
|
|
|
431
|
-
assert.equal(result.meta.total, 5)
|
|
432
|
-
assert.equal(result.meta.succeeded, 3)
|
|
433
|
-
assert.equal(result.meta.failed, 2)
|
|
434
|
-
assert.equal(result.meta.deleted.length, 3)
|
|
431
|
+
assert.equal(result.meta.total, 5)
|
|
432
|
+
assert.equal(result.meta.succeeded, 3)
|
|
433
|
+
assert.equal(result.meta.failed, 2)
|
|
434
|
+
assert.equal(result.meta.deleted.length, 3)
|
|
435
435
|
|
|
436
|
-
assert.equal(result.errors.length, 2)
|
|
437
|
-
assert.equal(result.errors[0].id, '999999')
|
|
438
|
-
assert.equal(result.errors[1].id, '888888')
|
|
436
|
+
assert.equal(result.errors.length, 2)
|
|
437
|
+
assert.equal(result.errors[0].id, '999999')
|
|
438
|
+
assert.equal(result.errors[1].id, '888888')
|
|
439
439
|
|
|
440
440
|
// Verify correct records were deleted via API
|
|
441
|
-
const queryResult = await api.resources.authors.query({ simplified: false })
|
|
442
|
-
assert.equal(queryResult.data.length, 2)
|
|
443
|
-
})
|
|
441
|
+
const queryResult = await api.resources.authors.query({ simplified: false })
|
|
442
|
+
assert.equal(queryResult.data.length, 2)
|
|
443
|
+
})
|
|
444
444
|
|
|
445
445
|
it('should rollback all deletes on error in atomic mode', async () => {
|
|
446
446
|
const idsToDelete = [
|
|
447
447
|
testData.authorIds[0],
|
|
448
448
|
testData.authorIds[1],
|
|
449
449
|
'999999' // Will fail
|
|
450
|
-
]
|
|
450
|
+
]
|
|
451
451
|
|
|
452
452
|
try {
|
|
453
453
|
await api.scopes.authors.bulkDelete({
|
|
454
454
|
ids: idsToDelete,
|
|
455
455
|
atomic: true
|
|
456
|
-
})
|
|
457
|
-
assert.fail('Should have thrown error')
|
|
456
|
+
})
|
|
457
|
+
assert.fail('Should have thrown error')
|
|
458
458
|
} catch (error) {
|
|
459
459
|
// Expected error
|
|
460
460
|
}
|
|
461
461
|
|
|
462
462
|
// Verify no records were deleted via API
|
|
463
|
-
const queryResult = await api.resources.authors.query({ simplified: false })
|
|
464
|
-
assert.equal(queryResult.data.length, 5, 'Should still have all 5 records')
|
|
465
|
-
})
|
|
463
|
+
const queryResult = await api.resources.authors.query({ simplified: false })
|
|
464
|
+
assert.equal(queryResult.data.length, 5, 'Should still have all 5 records')
|
|
465
|
+
})
|
|
466
466
|
|
|
467
467
|
it('should handle empty ID array', async () => {
|
|
468
468
|
try {
|
|
469
469
|
await api.scopes.authors.bulkDelete({
|
|
470
470
|
ids: []
|
|
471
|
-
})
|
|
472
|
-
assert.fail('Should have thrown error')
|
|
471
|
+
})
|
|
472
|
+
assert.fail('Should have thrown error')
|
|
473
473
|
} catch (error) {
|
|
474
|
-
assert(error.message.includes('array'))
|
|
474
|
+
assert(error.message.includes('array'))
|
|
475
475
|
}
|
|
476
|
-
})
|
|
476
|
+
})
|
|
477
477
|
|
|
478
478
|
it('should respect relationships when deleting', async () => {
|
|
479
479
|
// Create country and publishers with relationship
|
|
480
480
|
const country = await api.resources.countries.post({
|
|
481
481
|
inputRecord: createJsonApiDocument('countries', { name: 'Test Country', code: 'TC' }),
|
|
482
482
|
simplified: false
|
|
483
|
-
})
|
|
483
|
+
})
|
|
484
484
|
|
|
485
485
|
const publishers = await Promise.all([
|
|
486
486
|
api.resources.publishers.post({
|
|
@@ -497,7 +497,7 @@ describe('Bulk Operations', () => {
|
|
|
497
497
|
),
|
|
498
498
|
simplified: false
|
|
499
499
|
})
|
|
500
|
-
])
|
|
500
|
+
])
|
|
501
501
|
|
|
502
502
|
// Create books referencing publishers
|
|
503
503
|
const book = await api.resources.books.post({
|
|
@@ -509,70 +509,70 @@ describe('Bulk Operations', () => {
|
|
|
509
509
|
}
|
|
510
510
|
),
|
|
511
511
|
simplified: false
|
|
512
|
-
})
|
|
512
|
+
})
|
|
513
513
|
|
|
514
514
|
// Try to delete publisher that has books
|
|
515
|
-
const publisherIds = publishers.map(p => p.data.id)
|
|
516
|
-
|
|
515
|
+
const publisherIds = publishers.map(p => p.data.id)
|
|
516
|
+
|
|
517
517
|
const result = await api.scopes.publishers.bulkDelete({
|
|
518
518
|
ids: publisherIds,
|
|
519
519
|
atomic: false
|
|
520
|
-
})
|
|
520
|
+
})
|
|
521
521
|
|
|
522
522
|
// At least one should succeed (the one without books)
|
|
523
|
-
assert(result.meta.succeeded >= 1)
|
|
524
|
-
})
|
|
525
|
-
})
|
|
523
|
+
assert(result.meta.succeeded >= 1)
|
|
524
|
+
})
|
|
525
|
+
})
|
|
526
526
|
|
|
527
527
|
describe('Batch Processing', () => {
|
|
528
528
|
beforeEach(async () => {
|
|
529
529
|
await cleanTables(knex, [
|
|
530
530
|
'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors'
|
|
531
|
-
])
|
|
532
|
-
})
|
|
531
|
+
])
|
|
532
|
+
})
|
|
533
533
|
|
|
534
534
|
it('should process large batches correctly', async () => {
|
|
535
535
|
// Create 50 records to test batching (batch size is 10 in config)
|
|
536
|
-
const records = Array.from({ length: 50 }, (_, i) =>
|
|
536
|
+
const records = Array.from({ length: 50 }, (_, i) =>
|
|
537
537
|
createJsonApiDocument('authors', { name: `Batch Author ${i + 1}` })
|
|
538
|
-
)
|
|
538
|
+
)
|
|
539
539
|
|
|
540
540
|
const result = await api.scopes.authors.bulkPost({
|
|
541
541
|
inputRecords: records,
|
|
542
542
|
atomic: true
|
|
543
|
-
})
|
|
543
|
+
})
|
|
544
544
|
|
|
545
|
-
assert.equal(result.meta.total, 50)
|
|
546
|
-
assert.equal(result.meta.succeeded, 50)
|
|
547
|
-
assert.equal(result.data.length, 50)
|
|
545
|
+
assert.equal(result.meta.total, 50)
|
|
546
|
+
assert.equal(result.meta.succeeded, 50)
|
|
547
|
+
assert.equal(result.data.length, 50)
|
|
548
548
|
|
|
549
549
|
// Verify via API with increased page size
|
|
550
|
-
const queryResult = await api.resources.authors.query({
|
|
550
|
+
const queryResult = await api.resources.authors.query({
|
|
551
551
|
simplified: false,
|
|
552
552
|
queryParams: { page: { size: 100 } }
|
|
553
|
-
})
|
|
554
|
-
assert.equal(queryResult.data.length, 50)
|
|
555
|
-
})
|
|
556
|
-
})
|
|
553
|
+
})
|
|
554
|
+
assert.equal(queryResult.data.length, 50)
|
|
555
|
+
})
|
|
556
|
+
})
|
|
557
557
|
|
|
558
558
|
describe('Complex Scenarios', () => {
|
|
559
559
|
beforeEach(async () => {
|
|
560
560
|
await cleanTables(knex, [
|
|
561
561
|
'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors'
|
|
562
|
-
])
|
|
563
|
-
})
|
|
562
|
+
])
|
|
563
|
+
})
|
|
564
564
|
|
|
565
565
|
it('should handle mixed operations workflow', async () => {
|
|
566
566
|
// Step 1: Bulk create authors
|
|
567
|
-
const authorRecords = Array.from({ length: 5 }, (_, i) =>
|
|
567
|
+
const authorRecords = Array.from({ length: 5 }, (_, i) =>
|
|
568
568
|
createJsonApiDocument('authors', { name: `Author ${i + 1}` })
|
|
569
|
-
)
|
|
569
|
+
)
|
|
570
570
|
|
|
571
571
|
const createResult = await api.scopes.authors.bulkPost({
|
|
572
572
|
inputRecords: authorRecords
|
|
573
|
-
})
|
|
573
|
+
})
|
|
574
574
|
|
|
575
|
-
const authorIds = createResult.data.map(a => a.id)
|
|
575
|
+
const authorIds = createResult.data.map(a => a.id)
|
|
576
576
|
|
|
577
577
|
// Step 2: Bulk update some of them
|
|
578
578
|
const updateOperations = [
|
|
@@ -592,29 +592,29 @@ describe('Bulk Operations', () => {
|
|
|
592
592
|
attributes: { name: 'Senior Author 3' }
|
|
593
593
|
}
|
|
594
594
|
}
|
|
595
|
-
]
|
|
595
|
+
]
|
|
596
596
|
|
|
597
597
|
const updateResult = await api.scopes.authors.bulkPatch({
|
|
598
598
|
operations: updateOperations
|
|
599
|
-
})
|
|
599
|
+
})
|
|
600
600
|
|
|
601
|
-
assert.equal(updateResult.meta.succeeded, 2)
|
|
601
|
+
assert.equal(updateResult.meta.succeeded, 2)
|
|
602
602
|
|
|
603
603
|
// Step 3: Bulk delete some others
|
|
604
604
|
const deleteResult = await api.scopes.authors.bulkDelete({
|
|
605
605
|
ids: [authorIds[3], authorIds[4]]
|
|
606
|
-
})
|
|
606
|
+
})
|
|
607
607
|
|
|
608
|
-
assert.equal(deleteResult.meta.succeeded, 2)
|
|
608
|
+
assert.equal(deleteResult.meta.succeeded, 2)
|
|
609
609
|
|
|
610
610
|
// Verify final state via API
|
|
611
|
-
const finalQuery = await api.resources.authors.query({ simplified: false })
|
|
612
|
-
assert.equal(finalQuery.data.length, 3)
|
|
611
|
+
const finalQuery = await api.resources.authors.query({ simplified: false })
|
|
612
|
+
assert.equal(finalQuery.data.length, 3)
|
|
613
613
|
|
|
614
614
|
// Check for senior authors
|
|
615
|
-
const seniorAuthors = finalQuery.data.filter(a => a.attributes.name.includes('Senior'))
|
|
616
|
-
assert.equal(seniorAuthors.length, 2)
|
|
617
|
-
})
|
|
615
|
+
const seniorAuthors = finalQuery.data.filter(a => a.attributes.name.includes('Senior'))
|
|
616
|
+
assert.equal(seniorAuthors.length, 2)
|
|
617
|
+
})
|
|
618
618
|
|
|
619
619
|
it('should maintain data integrity with relationships', async () => {
|
|
620
620
|
// Create countries
|
|
@@ -623,9 +623,9 @@ describe('Bulk Operations', () => {
|
|
|
623
623
|
createJsonApiDocument('countries', { name: 'USA', code: 'US' }),
|
|
624
624
|
createJsonApiDocument('countries', { name: 'UK', code: 'GB' })
|
|
625
625
|
]
|
|
626
|
-
})
|
|
626
|
+
})
|
|
627
627
|
|
|
628
|
-
const countryIds = countryResult.data.map(c => c.id)
|
|
628
|
+
const countryIds = countryResult.data.map(c => c.id)
|
|
629
629
|
|
|
630
630
|
// Create publishers with country relationships
|
|
631
631
|
const publisherRecords = [
|
|
@@ -637,21 +637,21 @@ describe('Bulk Operations', () => {
|
|
|
637
637
|
{ name: 'UK Publisher' },
|
|
638
638
|
{ country: createRelationship(resourceIdentifier('countries', countryIds[1])) }
|
|
639
639
|
)
|
|
640
|
-
]
|
|
640
|
+
]
|
|
641
641
|
|
|
642
642
|
const publisherResult = await api.scopes.publishers.bulkPost({
|
|
643
643
|
inputRecords: publisherRecords
|
|
644
|
-
})
|
|
644
|
+
})
|
|
645
645
|
|
|
646
|
-
assert.equal(publisherResult.data.length, 2)
|
|
646
|
+
assert.equal(publisherResult.data.length, 2)
|
|
647
647
|
|
|
648
648
|
// Verify relationships were set correctly via API
|
|
649
|
-
const allPublishers = await api.resources.publishers.query({
|
|
649
|
+
const allPublishers = await api.resources.publishers.query({
|
|
650
650
|
simplified: false
|
|
651
|
-
})
|
|
652
|
-
const usPublisher = allPublishers.data.find(p => p.attributes.name === 'US Publisher')
|
|
653
|
-
assert(usPublisher, 'Should find US Publisher')
|
|
654
|
-
assert.equal(usPublisher.relationships.country.data.id, countryIds[0])
|
|
655
|
-
})
|
|
656
|
-
})
|
|
657
|
-
})
|
|
651
|
+
})
|
|
652
|
+
const usPublisher = allPublishers.data.find(p => p.attributes.name === 'US Publisher')
|
|
653
|
+
assert(usPublisher, 'Should find US Publisher')
|
|
654
|
+
assert.equal(usPublisher.relationships.country.data.id, countryIds[0])
|
|
655
|
+
})
|
|
656
|
+
})
|
|
657
|
+
})
|