json-rest-api 1.0.28 → 2.0.0

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.
Files changed (289) hide show
  1. package/LICENSE +10 -670
  2. package/LICENSE-GPL-3.0 +674 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +39 -30
  5. package/docs/API.md +746 -1273
  6. package/docs/GUIDE/01-getting-started.md +338 -0
  7. package/docs/GUIDE/02-resources-and-relationships.md +33 -0
  8. package/docs/GUIDE/03-running-example.md +87 -0
  9. package/docs/GUIDE/04-creating-and-querying.md +181 -0
  10. package/docs/GUIDE/05-belongs-to.md +177 -0
  11. package/docs/GUIDE/06-has-many.md +184 -0
  12. package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
  13. package/docs/GUIDE/08-many-to-many.md +184 -0
  14. package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
  15. package/docs/GUIDE/10-put-and-patch.md +174 -0
  16. package/docs/GUIDE/11-relationship-endpoints.md +174 -0
  17. package/docs/GUIDE/12-field-transformations.md +287 -0
  18. package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
  19. package/docs/GUIDE/14-custom-resource-methods.md +125 -0
  20. package/docs/GUIDE/15-query-projections.md +266 -0
  21. package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
  22. package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
  23. package/docs/GUIDE/18-labels.md +86 -0
  24. package/docs/GUIDE/19-managed-transactions.md +179 -0
  25. package/docs/GUIDE/20-transaction-outcomes.md +207 -0
  26. package/docs/GUIDE/21-schema-and-migrations.md +634 -0
  27. package/docs/GUIDE/22-version-field-migrations.md +208 -0
  28. package/docs/GUIDE/23-fastify.md +179 -0
  29. package/docs/GUIDE/24-cors.md +172 -0
  30. package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
  31. package/docs/GUIDE/26-file-uploads.md +278 -0
  32. package/docs/GUIDE/27-bulk-operations.md +277 -0
  33. package/docs/GUIDE/28-socketio.md +325 -0
  34. package/docs/GUIDE/29-writing-plugins.md +278 -0
  35. package/docs/GUIDE/30-backend-capabilities.md +100 -0
  36. package/docs/GUIDE/31-positioning.md +181 -0
  37. package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +90 -31
  38. package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
  39. package/docs/GUIDE/index.md +51 -59
  40. package/docs/QUICKSTART.md +120 -287
  41. package/docs/architecture.md +75 -0
  42. package/docs/contributing.md +178 -0
  43. package/index.d.ts +36 -0
  44. package/index.js +1 -22
  45. package/lib/enhanced-logger.js +91 -85
  46. package/lib/error-context.js +445 -5
  47. package/lib/error-formatter.js +219 -59
  48. package/lib/knex-transaction.js +64 -0
  49. package/lib/logger-types.d.ts +22 -0
  50. package/lib/missing-package.js +4 -0
  51. package/lib/rest-api-errors.js +103 -8
  52. package/lib/runtime/json-rest-api.js +172 -0
  53. package/lib/transaction-types.d.ts +59 -0
  54. package/package.json +97 -27
  55. package/plugins/core/bulk-operations-plugin.js +191 -184
  56. package/plugins/core/connectors/express-plugin.js +148 -219
  57. package/plugins/core/connectors/fastify-plugin.js +125 -82
  58. package/plugins/core/connectors/lib/busboy-detector.js +74 -156
  59. package/plugins/core/connectors/lib/connector-core.js +144 -20
  60. package/plugins/core/connectors/lib/formidable-detector.js +63 -120
  61. package/plugins/core/connectors/lib/http-validators.js +77 -0
  62. package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
  63. package/plugins/core/connectors/lib/request-helpers.js +10 -1
  64. package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
  65. package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
  66. package/plugins/core/file-handling-plugin.js +87 -174
  67. package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
  68. package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
  69. package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
  70. package/plugins/core/lib/anyapi/schema-utils.js +25 -16
  71. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  72. package/plugins/core/lib/dbIntrospection.js +228 -21
  73. package/plugins/core/lib/dbTablesOperations.js +409 -92
  74. package/plugins/core/lib/fractional-positioning.js +12 -48
  75. package/plugins/core/lib/querying/identifier-query.js +19 -0
  76. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  77. package/plugins/core/lib/querying/include-to-many.js +428 -0
  78. package/plugins/core/lib/querying/include-to-one.js +579 -0
  79. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  80. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  81. package/plugins/core/lib/querying/knex-pagination-helpers.js +67 -330
  82. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  83. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  84. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  85. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  86. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  87. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  88. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  89. package/plugins/core/lib/querying/query-constraint.js +27 -0
  90. package/plugins/core/lib/querying/query-field-sort-helpers.js +205 -30
  91. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  92. package/plugins/core/lib/querying/sort-helpers.js +20 -1
  93. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  94. package/plugins/core/lib/querying/url-helpers.js +11 -4
  95. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  96. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  97. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  98. package/plugins/core/lib/querying-writing/database-value-normalizers.js +238 -187
  99. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  100. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  101. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  102. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  103. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  104. package/plugins/core/lib/querying-writing/knex-field-helpers.js +111 -287
  105. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  106. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  107. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  108. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  109. package/plugins/core/lib/querying-writing/request-contracts.js +109 -44
  110. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  111. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  112. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  113. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  114. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  115. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  116. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  117. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  118. package/plugins/core/lib/storage/storage-adapter.js +142 -62
  119. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  120. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  121. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  122. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  123. package/plugins/core/lib/writing/resource-version.js +172 -0
  124. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  125. package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2509
  126. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  127. package/plugins/core/rest-api-cors-plugin.js +52 -205
  128. package/plugins/core/rest-api-knex-plugin.js +45 -1069
  129. package/plugins/core/rest-api-label-plugin.js +25 -38
  130. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  131. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  132. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  133. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  134. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  135. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  136. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  137. package/plugins/core/rest-api-plugin-methods/common.js +291 -385
  138. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  139. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  140. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -145
  141. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  142. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  143. package/plugins/core/rest-api-plugin-methods/get.js +38 -69
  144. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  145. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  146. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  147. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  148. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  149. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  150. package/plugins/core/rest-api-plugin-methods/query.js +39 -82
  151. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  152. package/plugins/core/rest-api-plugin.js +32 -46
  153. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  154. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  155. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  156. package/plugins/core/socketio-plugin.js +364 -344
  157. package/plugins/storage/local-storage.js +40 -36
  158. package/types/autofilter.d.ts +57 -0
  159. package/types/bulk-methods.d.ts +63 -0
  160. package/types/errors.d.ts +118 -0
  161. package/types/fastify-options.d.ts +19 -0
  162. package/types/file-detectors.d.ts +33 -0
  163. package/types/file-storage.d.ts +48 -0
  164. package/types/hook-context.d.ts +180 -0
  165. package/types/plugin-options.d.ts +53 -0
  166. package/types/relationship-methods.d.ts +74 -0
  167. package/types/representations.d.ts +50 -0
  168. package/types/resource-methods.d.ts +95 -0
  169. package/types/resource-schema.d.ts +157 -0
  170. package/types/row-policy.d.ts +31 -0
  171. package/types/runtime.d.ts +132 -0
  172. package/types/transactions.d.ts +21 -0
  173. package/.claude/agents/library-guide-expert.md +0 -30
  174. package/.claude/agents/rest-api-internals-expert.md +0 -54
  175. package/.claude/settings.local.json +0 -70
  176. package/AGENTS.md +0 -34
  177. package/CLAUDE.md +0 -160
  178. package/CRUSH.md +0 -55
  179. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  180. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  181. package/TODO.todo +0 -13
  182. package/connectors_ideas/fastify.js +0 -217
  183. package/connectors_ideas/koa-plugin.js +0 -112
  184. package/docs/.bundle/config +0 -2
  185. package/docs/COMPARISON.md +0 -433
  186. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  187. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  188. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  189. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  190. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  191. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  192. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  193. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1238
  194. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  195. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  196. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  197. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  198. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  199. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  200. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  201. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  202. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  203. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  204. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  205. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  206. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  207. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  208. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  209. package/docs/Gemfile +0 -6
  210. package/docs/Gemfile.lock +0 -308
  211. package/docs/ONBOARDING.md +0 -315
  212. package/docs/_config.yml +0 -36
  213. package/docs/_layouts/default.html +0 -123
  214. package/docs/apple-touch-icon.svg +0 -4
  215. package/docs/assets/css/style.scss +0 -392
  216. package/docs/favicon.svg +0 -4
  217. package/docs/index.md +0 -164
  218. package/docs/mini-guides/file-uploads-complete.md +0 -161
  219. package/eslint.config.js +0 -13
  220. package/examples/url-override-example.js +0 -137
  221. package/fixing_plan.md +0 -112
  222. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  223. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  224. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  225. package/plugins/storage/s3-storage.js +0 -93
  226. package/quickTest.js +0 -136
  227. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  228. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  229. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  230. package/scripts/run-docs.js +0 -133
  231. package/test-debug.js +0 -59
  232. package/tests/README.md +0 -202
  233. package/tests/TEST_TEMPLATE.test.js +0 -178
  234. package/tests/anyapi-basic.test.js +0 -105
  235. package/tests/anyapi-cursor-pagination.test.js +0 -150
  236. package/tests/anyapi-custom-idproperty.test.js +0 -163
  237. package/tests/autofilter.test.js +0 -463
  238. package/tests/bulk-operations.test.js +0 -657
  239. package/tests/computed-fields.test.js +0 -586
  240. package/tests/cors.test.js +0 -834
  241. package/tests/curl.test.js +0 -467
  242. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  243. package/tests/custom-id-simplified-post.test.js +0 -75
  244. package/tests/custom-idproperty-relationships.test.js +0 -932
  245. package/tests/db-introspection.test.js +0 -521
  246. package/tests/db-table-operations.test.js +0 -798
  247. package/tests/fastify-plugin.test.js +0 -184
  248. package/tests/field-getters.test.js +0 -370
  249. package/tests/field-setters.test.js +0 -306
  250. package/tests/file-handling.test.js +0 -118
  251. package/tests/fixtures/api-configs.js +0 -2509
  252. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  253. package/tests/helpers/fake-fastify.js +0 -99
  254. package/tests/helpers/storage-mode.js +0 -65
  255. package/tests/helpers/test-utils.js +0 -203
  256. package/tests/http-connectors-parity.test.js +0 -395
  257. package/tests/id-normalization.test.js +0 -444
  258. package/tests/include-depth-validation.test.js +0 -331
  259. package/tests/includes.test.js +0 -427
  260. package/tests/local-storage.test.js +0 -96
  261. package/tests/logical-id-write-validation.test.js +0 -151
  262. package/tests/nested-includes.test.js +0 -617
  263. package/tests/pagination-cursor-multifield.test.js +0 -643
  264. package/tests/pagination-enhanced.test.js +0 -563
  265. package/tests/polymorphic-relationships.test.js +0 -347
  266. package/tests/polymorphic-simple.test.js +0 -148
  267. package/tests/positioning-advanced.test.js +0 -692
  268. package/tests/positioning-extended.test.js +0 -915
  269. package/tests/positioning-multifilter.test.js +0 -699
  270. package/tests/positioning.test.js +0 -560
  271. package/tests/post-commit-hooks.test.js +0 -120
  272. package/tests/projected-fields.test.js +0 -319
  273. package/tests/public-exports.test.js +0 -15
  274. package/tests/put-create-parity.test.js +0 -216
  275. package/tests/queries.test.js +0 -521
  276. package/tests/query-limits.test.js +0 -584
  277. package/tests/relationship-endpoints.test.js +0 -620
  278. package/tests/relationship-include-storage-adapter.test.js +0 -56
  279. package/tests/relationships.test.js +0 -681
  280. package/tests/rest-api.test.js +0 -337
  281. package/tests/return-record-settings.test.js +0 -184
  282. package/tests/row-policy.test.js +0 -489
  283. package/tests/s3-storage.test.js +0 -38
  284. package/tests/searchschema-knex-behavior.test.js +0 -187
  285. package/tests/searchschema-merge.test.js +0 -151
  286. package/tests/socketio.test.js +0 -860
  287. package/tests/storage-mapping.test.js +0 -362
  288. package/tests/test-development-guide.md +0 -370
  289. package/tests/virtual-fields.test.js +0 -404
@@ -1,657 +0,0 @@
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
- import {
6
- validateJsonApiStructure,
7
- resourceIdentifier,
8
- cleanTables,
9
- createJsonApiDocument,
10
- createRelationship,
11
- createToManyRelationship
12
- } from './helpers/test-utils.js'
13
-
14
- // Create Knex instance for tests
15
- const knex = knexLib({
16
- client: 'better-sqlite3',
17
- connection: {
18
- filename: ':memory:'
19
- },
20
- useNullAsDefault: true
21
- })
22
-
23
- // API instance that persists across tests
24
- let api
25
-
26
- describe('Bulk Operations', () => {
27
- before(async () => {
28
- // Initialize API once
29
- api = await createBulkOperationsApi(knex)
30
- })
31
-
32
- after(async () => {
33
- // Close database connection to allow tests to exit
34
- await knex.destroy()
35
- })
36
-
37
- describe('Bulk Create (bulkPost)', () => {
38
- beforeEach(async () => {
39
- // Clean all tables before each test
40
- await cleanTables(knex, [
41
- 'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors'
42
- ])
43
- })
44
-
45
- it('should create multiple records atomically', async () => {
46
- const records = [
47
- createJsonApiDocument('authors', { name: 'Author One' }),
48
- createJsonApiDocument('authors', { name: 'Author Two' }),
49
- createJsonApiDocument('authors', { name: 'Author Three' })
50
- ]
51
-
52
- const result = await api.scopes.authors.bulkPost({
53
- inputRecords: records,
54
- atomic: true
55
- })
56
-
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')
61
-
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)
67
-
68
- // Validate each created record
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
- })
75
-
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
- })
80
-
81
- it('should rollback all records on error in atomic mode', async () => {
82
- const records = [
83
- createJsonApiDocument('authors', { name: 'Valid Author' }),
84
- createJsonApiDocument('authors', {}), // Invalid - missing required name
85
- createJsonApiDocument('authors', { name: 'Another Valid' })
86
- ]
87
-
88
- try {
89
- await api.scopes.authors.bulkPost({
90
- inputRecords: records,
91
- atomic: true
92
- })
93
- assert.fail('Should have thrown error')
94
- } catch (error) {
95
- assert(error.message.includes('validation failed') || error.message.includes('required'), 'Error should mention validation or required field')
96
- }
97
-
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
- })
102
-
103
- it('should allow partial success in non-atomic mode', async () => {
104
- const records = [
105
- createJsonApiDocument('authors', { name: 'Valid Author 1' }),
106
- createJsonApiDocument('authors', {}), // Invalid
107
- createJsonApiDocument('authors', { name: 'Valid Author 2' }),
108
- createJsonApiDocument('authors', { name: 'Valid Author 3' })
109
- ]
110
-
111
- const result = await api.scopes.authors.bulkPost({
112
- inputRecords: records,
113
- atomic: false
114
- })
115
-
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)
120
-
121
- // Check errors
122
- assert(result.errors, 'Should have errors array')
123
- assert.equal(result.errors.length, 1)
124
-
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'))
128
-
129
- // Verify via API
130
- const queryResult = await api.resources.authors.query({ simplified: false })
131
- assert.equal(queryResult.data.length, 3)
132
- })
133
-
134
- it('should create records with relationships', async () => {
135
- // First create a country
136
- const countryResult = await api.resources.countries.post({
137
- inputRecord: createJsonApiDocument('countries', { name: 'Test Country', code: 'TC' }),
138
- simplified: false
139
- })
140
-
141
- const records = [
142
- createJsonApiDocument('publishers',
143
- { name: 'Publisher One' },
144
- { country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
145
- ),
146
- createJsonApiDocument('publishers',
147
- { name: 'Publisher Two' },
148
- { country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
149
- )
150
- ]
151
-
152
- const result = await api.scopes.publishers.bulkPost({
153
- inputRecords: records
154
- })
155
-
156
- assert.equal(result.data.length, 2)
157
- result.data.forEach(publisher => {
158
- assert.equal(publisher.relationships.country.data.id, countryResult.data.id)
159
- })
160
- })
161
-
162
- it('should enforce max bulk operations limit', async () => {
163
- const tooManyRecords = Array.from({ length: 101 }, (_, i) =>
164
- createJsonApiDocument('authors', { name: `Author ${i}` })
165
- )
166
-
167
- try {
168
- await api.scopes.authors.bulkPost({
169
- inputRecords: tooManyRecords
170
- })
171
- assert.fail('Should have thrown error')
172
- } catch (error) {
173
- assert(error.message.includes('limited to 100'))
174
- }
175
- })
176
-
177
- it('should respect atomic query parameter override', async () => {
178
- const records = [
179
- createJsonApiDocument('authors', { name: 'Valid' }),
180
- createJsonApiDocument('authors', {}) // Invalid
181
- ]
182
-
183
- // Override default atomic mode
184
- const result = await api.scopes.authors.bulkPost({
185
- inputRecords: records,
186
- atomic: false // Override to non-atomic
187
- })
188
-
189
- assert.equal(result.meta.succeeded, 1)
190
- assert.equal(result.meta.failed, 1)
191
- assert.equal(result.meta.atomic, false)
192
- })
193
- })
194
-
195
- describe('Bulk Update (bulkPatch)', () => {
196
- const testData = {}
197
-
198
- beforeEach(async () => {
199
- await cleanTables(knex, [
200
- 'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors'
201
- ])
202
-
203
- // Create test data via API
204
- const authors = await Promise.all([
205
- api.resources.authors.post({
206
- inputRecord: createJsonApiDocument('authors', { name: 'Author One' }),
207
- simplified: false
208
- }),
209
- api.resources.authors.post({
210
- inputRecord: createJsonApiDocument('authors', { name: 'Author Two' }),
211
- simplified: false
212
- }),
213
- api.resources.authors.post({
214
- inputRecord: createJsonApiDocument('authors', { name: 'Author Three' }),
215
- simplified: false
216
- })
217
- ])
218
-
219
- testData.authorIds = authors.map(a => a.data.id)
220
- })
221
-
222
- it('should update multiple records atomically', async () => {
223
- const operations = [
224
- {
225
- id: testData.authorIds[0],
226
- data: {
227
- type: 'authors',
228
- id: testData.authorIds[0],
229
- attributes: { name: 'Updated Author One' }
230
- }
231
- },
232
- {
233
- id: testData.authorIds[1],
234
- data: {
235
- type: 'authors',
236
- id: testData.authorIds[1],
237
- attributes: { name: 'Updated Author Two' }
238
- }
239
- }
240
- ]
241
-
242
- const result = await api.scopes.authors.bulkPatch({
243
- operations,
244
- atomic: true
245
- })
246
-
247
- assert.equal(result.data.length, 2)
248
- assert.equal(result.meta.succeeded, 2)
249
- assert.equal(result.meta.failed, 0)
250
-
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')
254
-
255
- // Verify via API
256
- const author1 = await api.resources.authors.get({
257
- id: testData.authorIds[0],
258
- simplified: false
259
- })
260
- assert.equal(author1.data.attributes.name, 'Updated Author One')
261
- })
262
-
263
- it('should handle non-existent IDs in non-atomic mode', async () => {
264
- const operations = [
265
- {
266
- id: testData.authorIds[0],
267
- data: {
268
- type: 'authors',
269
- id: testData.authorIds[0],
270
- attributes: { name: 'Updated' }
271
- }
272
- },
273
- {
274
- id: '999999', // Non-existent
275
- data: {
276
- type: 'authors',
277
- id: '999999',
278
- attributes: { name: 'Should Fail' }
279
- }
280
- },
281
- {
282
- id: testData.authorIds[2],
283
- data: {
284
- type: 'authors',
285
- id: testData.authorIds[2],
286
- attributes: { name: 'Also Updated' }
287
- }
288
- }
289
- ]
290
-
291
- const result = await api.scopes.authors.bulkPatch({
292
- operations,
293
- atomic: false
294
- })
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
- })
301
-
302
- it('should validate operation structure', async () => {
303
- const invalidOperations = [
304
- { id: testData.authorIds[0] }, // Missing data
305
- { data: { type: 'authors', attributes: { name: 'Test' } } }, // Missing id
306
- {
307
- id: testData.authorIds[1],
308
- data: {
309
- type: 'authors',
310
- id: testData.authorIds[1],
311
- attributes: { name: 'Valid' }
312
- }
313
- }
314
- ]
315
-
316
- const result = await api.scopes.authors.bulkPatch({
317
- operations: invalidOperations,
318
- atomic: false
319
- })
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
- })
326
-
327
- it('should rollback all updates on error in atomic mode', async () => {
328
- // Get original name via API
329
- const originalAuthor = await api.resources.authors.get({
330
- id: testData.authorIds[0],
331
- simplified: false
332
- })
333
-
334
- const operations = [
335
- {
336
- id: testData.authorIds[0],
337
- data: {
338
- type: 'authors',
339
- id: testData.authorIds[0],
340
- attributes: { name: 'Should Be Rolled Back' }
341
- }
342
- },
343
- {
344
- id: '999999', // Will fail
345
- data: {
346
- type: 'authors',
347
- id: '999999',
348
- attributes: { name: 'Non-existent' }
349
- }
350
- }
351
- ]
352
-
353
- try {
354
- await api.scopes.authors.bulkPatch({
355
- operations,
356
- atomic: true
357
- })
358
- assert.fail('Should have thrown error')
359
- } catch (error) {
360
- // Expected error
361
- }
362
-
363
- // Verify no changes were made via API
364
- const author = await api.resources.authors.get({
365
- id: testData.authorIds[0],
366
- simplified: false
367
- })
368
- assert.equal(author.data.attributes.name, originalAuthor.data.attributes.name, 'Name should not have changed')
369
- })
370
- })
371
-
372
- describe('Bulk Delete (bulkDelete)', () => {
373
- const testData = {}
374
-
375
- beforeEach(async () => {
376
- await cleanTables(knex, [
377
- 'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors'
378
- ])
379
-
380
- // Create test data via API
381
- const authors = await Promise.all(
382
- Array.from({ length: 5 }, (_, i) =>
383
- api.resources.authors.post({
384
- inputRecord: createJsonApiDocument('authors', { name: `Author ${i + 1}` }),
385
- simplified: false
386
- })
387
- )
388
- )
389
-
390
- testData.authorIds = authors.map(a => a.data.id)
391
- })
392
-
393
- it('should delete multiple records atomically', async () => {
394
- const idsToDelete = [testData.authorIds[0], testData.authorIds[2], testData.authorIds[4]]
395
-
396
- const result = await api.scopes.authors.bulkDelete({
397
- ids: idsToDelete,
398
- atomic: true
399
- })
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)
405
-
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')
409
-
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
- })
416
-
417
- it('should handle mixed valid/invalid IDs in non-atomic mode', async () => {
418
- const idsToDelete = [
419
- testData.authorIds[0],
420
- '999999', // Non-existent
421
- testData.authorIds[2],
422
- '888888', // Non-existent
423
- testData.authorIds[4]
424
- ]
425
-
426
- const result = await api.scopes.authors.bulkDelete({
427
- ids: idsToDelete,
428
- atomic: false
429
- })
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)
435
-
436
- assert.equal(result.errors.length, 2)
437
- assert.equal(result.errors[0].id, '999999')
438
- assert.equal(result.errors[1].id, '888888')
439
-
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
- })
444
-
445
- it('should rollback all deletes on error in atomic mode', async () => {
446
- const idsToDelete = [
447
- testData.authorIds[0],
448
- testData.authorIds[1],
449
- '999999' // Will fail
450
- ]
451
-
452
- try {
453
- await api.scopes.authors.bulkDelete({
454
- ids: idsToDelete,
455
- atomic: true
456
- })
457
- assert.fail('Should have thrown error')
458
- } catch (error) {
459
- // Expected error
460
- }
461
-
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
- })
466
-
467
- it('should handle empty ID array', async () => {
468
- try {
469
- await api.scopes.authors.bulkDelete({
470
- ids: []
471
- })
472
- assert.fail('Should have thrown error')
473
- } catch (error) {
474
- assert(error.message.includes('array'))
475
- }
476
- })
477
-
478
- it('should respect relationships when deleting', async () => {
479
- // Create country and publishers with relationship
480
- const country = await api.resources.countries.post({
481
- inputRecord: createJsonApiDocument('countries', { name: 'Test Country', code: 'TC' }),
482
- simplified: false
483
- })
484
-
485
- const publishers = await Promise.all([
486
- api.resources.publishers.post({
487
- inputRecord: createJsonApiDocument('publishers',
488
- { name: 'Publisher 1' },
489
- { country: createRelationship(resourceIdentifier('countries', country.data.id)) }
490
- ),
491
- simplified: false
492
- }),
493
- api.resources.publishers.post({
494
- inputRecord: createJsonApiDocument('publishers',
495
- { name: 'Publisher 2' },
496
- { country: createRelationship(resourceIdentifier('countries', country.data.id)) }
497
- ),
498
- simplified: false
499
- })
500
- ])
501
-
502
- // Create books referencing publishers
503
- const book = await api.resources.books.post({
504
- inputRecord: createJsonApiDocument('books',
505
- { title: 'Test Book' },
506
- {
507
- country: createRelationship(resourceIdentifier('countries', country.data.id)),
508
- publisher: createRelationship(resourceIdentifier('publishers', publishers[0].data.id))
509
- }
510
- ),
511
- simplified: false
512
- })
513
-
514
- // Try to delete publisher that has books
515
- const publisherIds = publishers.map(p => p.data.id)
516
-
517
- const result = await api.scopes.publishers.bulkDelete({
518
- ids: publisherIds,
519
- atomic: false
520
- })
521
-
522
- // At least one should succeed (the one without books)
523
- assert(result.meta.succeeded >= 1)
524
- })
525
- })
526
-
527
- describe('Batch Processing', () => {
528
- beforeEach(async () => {
529
- await cleanTables(knex, [
530
- 'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors'
531
- ])
532
- })
533
-
534
- it('should process large batches correctly', async () => {
535
- // Create 50 records to test batching (batch size is 10 in config)
536
- const records = Array.from({ length: 50 }, (_, i) =>
537
- createJsonApiDocument('authors', { name: `Batch Author ${i + 1}` })
538
- )
539
-
540
- const result = await api.scopes.authors.bulkPost({
541
- inputRecords: records,
542
- atomic: true
543
- })
544
-
545
- assert.equal(result.meta.total, 50)
546
- assert.equal(result.meta.succeeded, 50)
547
- assert.equal(result.data.length, 50)
548
-
549
- // Verify via API with increased page size
550
- const queryResult = await api.resources.authors.query({
551
- simplified: false,
552
- queryParams: { page: { size: 100 } }
553
- })
554
- assert.equal(queryResult.data.length, 50)
555
- })
556
- })
557
-
558
- describe('Complex Scenarios', () => {
559
- beforeEach(async () => {
560
- await cleanTables(knex, [
561
- 'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors'
562
- ])
563
- })
564
-
565
- it('should handle mixed operations workflow', async () => {
566
- // Step 1: Bulk create authors
567
- const authorRecords = Array.from({ length: 5 }, (_, i) =>
568
- createJsonApiDocument('authors', { name: `Author ${i + 1}` })
569
- )
570
-
571
- const createResult = await api.scopes.authors.bulkPost({
572
- inputRecords: authorRecords
573
- })
574
-
575
- const authorIds = createResult.data.map(a => a.id)
576
-
577
- // Step 2: Bulk update some of them
578
- const updateOperations = [
579
- {
580
- id: authorIds[0],
581
- data: {
582
- type: 'authors',
583
- id: authorIds[0],
584
- attributes: { name: 'Senior Author 1' }
585
- }
586
- },
587
- {
588
- id: authorIds[2],
589
- data: {
590
- type: 'authors',
591
- id: authorIds[2],
592
- attributes: { name: 'Senior Author 3' }
593
- }
594
- }
595
- ]
596
-
597
- const updateResult = await api.scopes.authors.bulkPatch({
598
- operations: updateOperations
599
- })
600
-
601
- assert.equal(updateResult.meta.succeeded, 2)
602
-
603
- // Step 3: Bulk delete some others
604
- const deleteResult = await api.scopes.authors.bulkDelete({
605
- ids: [authorIds[3], authorIds[4]]
606
- })
607
-
608
- assert.equal(deleteResult.meta.succeeded, 2)
609
-
610
- // Verify final state via API
611
- const finalQuery = await api.resources.authors.query({ simplified: false })
612
- assert.equal(finalQuery.data.length, 3)
613
-
614
- // Check for senior authors
615
- const seniorAuthors = finalQuery.data.filter(a => a.attributes.name.includes('Senior'))
616
- assert.equal(seniorAuthors.length, 2)
617
- })
618
-
619
- it('should maintain data integrity with relationships', async () => {
620
- // Create countries
621
- const countryResult = await api.scopes.countries.bulkPost({
622
- inputRecords: [
623
- createJsonApiDocument('countries', { name: 'USA', code: 'US' }),
624
- createJsonApiDocument('countries', { name: 'UK', code: 'GB' })
625
- ]
626
- })
627
-
628
- const countryIds = countryResult.data.map(c => c.id)
629
-
630
- // Create publishers with country relationships
631
- const publisherRecords = [
632
- createJsonApiDocument('publishers',
633
- { name: 'US Publisher' },
634
- { country: createRelationship(resourceIdentifier('countries', countryIds[0])) }
635
- ),
636
- createJsonApiDocument('publishers',
637
- { name: 'UK Publisher' },
638
- { country: createRelationship(resourceIdentifier('countries', countryIds[1])) }
639
- )
640
- ]
641
-
642
- const publisherResult = await api.scopes.publishers.bulkPost({
643
- inputRecords: publisherRecords
644
- })
645
-
646
- assert.equal(publisherResult.data.length, 2)
647
-
648
- // Verify relationships were set correctly via API
649
- const allPublishers = await api.resources.publishers.query({
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
- })