json-rest-api 1.0.29 → 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 (292) 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 +744 -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} +74 -30
  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 -177
  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 +48 -317
  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 +134 -37
  91. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  92. package/plugins/core/lib/querying/sort-helpers.js +13 -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 +179 -194
  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 +100 -290
  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 +93 -38
  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 +128 -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 -2485
  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 -1077
  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 -395
  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 -148
  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 +36 -68
  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 +37 -81
  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 -1247
  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 -148
  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 -155
  251. package/tests/fixtures/api-configs.js +0 -2566
  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-boundaries.test.js +0 -154
  264. package/tests/pagination-contract.test.js +0 -122
  265. package/tests/pagination-cursor-multifield.test.js +0 -641
  266. package/tests/pagination-enhanced.test.js +0 -563
  267. package/tests/polymorphic-relationships.test.js +0 -347
  268. package/tests/polymorphic-simple.test.js +0 -148
  269. package/tests/positioning-advanced.test.js +0 -692
  270. package/tests/positioning-extended.test.js +0 -915
  271. package/tests/positioning-multifilter.test.js +0 -699
  272. package/tests/positioning.test.js +0 -560
  273. package/tests/post-commit-hooks.test.js +0 -120
  274. package/tests/projected-fields.test.js +0 -319
  275. package/tests/public-exports.test.js +0 -15
  276. package/tests/put-create-parity.test.js +0 -216
  277. package/tests/queries.test.js +0 -521
  278. package/tests/query-limits.test.js +0 -584
  279. package/tests/relationship-endpoints.test.js +0 -620
  280. package/tests/relationship-include-storage-adapter.test.js +0 -56
  281. package/tests/relationships.test.js +0 -681
  282. package/tests/rest-api.test.js +0 -337
  283. package/tests/return-record-settings.test.js +0 -184
  284. package/tests/row-policy.test.js +0 -489
  285. package/tests/s3-storage.test.js +0 -38
  286. package/tests/searchschema-knex-behavior.test.js +0 -187
  287. package/tests/searchschema-merge.test.js +0 -151
  288. package/tests/socketio.test.js +0 -860
  289. package/tests/storage-mapping.test.js +0 -362
  290. package/tests/temporal-boundaries.test.js +0 -208
  291. package/tests/test-development-guide.md +0 -370
  292. package/tests/virtual-fields.test.js +0 -404
@@ -1,834 +0,0 @@
1
- import { describe, it, before, after, beforeEach } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import knexLib from 'knex'
4
- import express from 'express'
5
- import request from 'supertest'
6
- import {
7
- validateJsonApiStructure,
8
- cleanTables,
9
- createJsonApiDocument,
10
- assertResourceAttributes
11
- } from './helpers/test-utils.js'
12
- import { createBasicApi } from './fixtures/api-configs.js'
13
- import { CorsPlugin } from '../plugins/core/rest-api-cors-plugin.js'
14
-
15
- // Create Knex instance for tests - always use SQLite in-memory
16
- const knex = knexLib({
17
- client: 'better-sqlite3',
18
- connection: {
19
- filename: ':memory:'
20
- },
21
- useNullAsDefault: true
22
- })
23
-
24
- // API instance and Express app that persist across tests
25
- let api
26
- let app
27
-
28
- describe('CORS Plugin Tests', { timeout: 30000 }, () => {
29
- describe('CORS with baseUrl', () => {
30
- let baseUrlApi
31
- let baseUrlApp
32
-
33
- before(async () => {
34
- // Create a new API instance with Express configured with a baseUrl
35
- baseUrlApp = express()
36
- // Create API with different name to avoid registry conflict
37
- const Api = (await import('hooked-api')).Api
38
- baseUrlApi = new Api({
39
- name: 'cors-baseurl-test-api',
40
- log: { level: 'info' }
41
- })
42
-
43
- // Install plugins
44
- await baseUrlApi.use((await import('../plugins/core/rest-api-plugin.js')).RestApiPlugin, {
45
- simplifiedApi: false
46
- })
47
- await baseUrlApi.use((await import('../plugins/core/rest-api-knex-plugin.js')).RestApiKnexPlugin, { knex })
48
- await baseUrlApi.use((await import('../plugins/core/connectors/express-plugin.js')).ExpressPlugin, {
49
- app: baseUrlApp,
50
- mountPath: '/api'
51
- })
52
-
53
- // Add basic scope
54
- await baseUrlApi.addScope('countries', {
55
- restApi: {
56
- schema: {
57
- attributes: {
58
- name: { type: 'string', required: true },
59
- code: { type: 'string', required: true }
60
- }
61
- }
62
- }
63
- })
64
-
65
- // Install CORS plugin
66
- console.log('[TEST] vars.transport before CORS install:', baseUrlApi.vars.transport)
67
-
68
- try {
69
- await baseUrlApi.use(CorsPlugin, {
70
- origin: '*',
71
- credentials: true
72
- })
73
- } catch (error) {
74
- console.log('[TEST] CORS installation error:', error.message)
75
- console.log('[TEST] vars.transport.matchAll:', baseUrlApi.vars.transport?.matchAll)
76
- console.log('[TEST] mountPath from transport:', baseUrlApi.vars.transport?.mountPath)
77
- console.log('[TEST] Full wildcard path would be:', '' + (baseUrlApi.vars.transport?.matchAll || '*'))
78
- throw error
79
- }
80
-
81
- // Mount the routes
82
- baseUrlApi.http.express.mount(baseUrlApp)
83
- })
84
-
85
- it('should handle OPTIONS preflight with baseUrl', async () => {
86
- const response = await request(baseUrlApp)
87
- .options('/api/countries')
88
- .set('Origin', 'https://example.com')
89
- .set('Access-Control-Request-Method', 'POST')
90
- .set('Access-Control-Request-Headers', 'Content-Type')
91
-
92
- console.log('[TEST] BaseUrl OPTIONS response:', {
93
- status: response.status,
94
- headers: response.headers,
95
- body: response.body
96
- })
97
-
98
- assert.equal(response.status, 204)
99
- assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
100
- assert.equal(response.headers['access-control-allow-credentials'], 'true')
101
- })
102
-
103
- it('should handle GET requests with baseUrl', async () => {
104
- // Just test the GET endpoint without creating data first
105
- // This tests CORS headers on the response regardless of data
106
- const response = await request(baseUrlApp)
107
- .get('/api/countries')
108
- .set('Origin', 'https://example.com')
109
- .set('Accept', 'application/vnd.api+json')
110
-
111
- console.log('[TEST] BaseUrl GET response:', {
112
- status: response.status,
113
- headers: response.headers
114
- })
115
-
116
- // We might get 500 due to missing table, but CORS headers should still be present
117
- assert(response.status === 200 || response.status === 500, `Expected 200 or 500, got ${response.status}`)
118
- assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
119
- assert.equal(response.headers['access-control-allow-credentials'], 'true')
120
- assert.equal(response.headers['vary'], 'Origin')
121
- })
122
-
123
- it('should handle wildcard OPTIONS route with baseUrl', async () => {
124
- // Test a non-existent endpoint to check if wildcard OPTIONS still works
125
- const response = await request(baseUrlApp)
126
- .options('/api/non-existent-endpoint')
127
- .set('Origin', 'https://example.com')
128
- .set('Access-Control-Request-Method', 'POST')
129
-
130
- console.log('[TEST] BaseUrl wildcard OPTIONS response:', {
131
- status: response.status,
132
- headers: response.headers
133
- })
134
-
135
- // This will tell us if the wildcard '*' route is working with baseUrl
136
- assert.equal(response.status, 204)
137
- assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
138
- })
139
- })
140
-
141
- // IMPORTANT: before() runs ONCE for the entire test suite
142
- before(async () => {
143
- // Initialize API once with Express
144
- app = express()
145
- api = await createBasicApi(knex, {
146
- includeExpress: true,
147
- express: { app }
148
- })
149
-
150
- // Install CORS plugin
151
- await api.use(CorsPlugin, {
152
-
153
- origin: '*',
154
- credentials: true,
155
- methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
156
- allowedHeaders: ['Content-Type', 'Authorization', 'X-Custom-Header'],
157
- exposedHeaders: ['X-Total-Count', 'X-Page-Count', 'Link'],
158
- maxAge: 86400
159
- }
160
- )
161
-
162
- // Mount the routes on the Express app
163
- api.http.express.mount(app)
164
- })
165
-
166
- // IMPORTANT: after() cleans up resources
167
- after(async () => {
168
- // Always destroy knex connection to allow tests to exit
169
- await knex.destroy()
170
- })
171
-
172
- // IMPORTANT: beforeEach() cleans data but does NOT recreate API
173
- beforeEach(async () => {
174
- // Clean all tables - list ALL tables your tests use
175
- await cleanTables(knex, [
176
- 'basic_countries',
177
- 'basic_publishers',
178
- 'basic_authors',
179
- 'basic_books',
180
- 'basic_book_authors'
181
- ])
182
- })
183
-
184
- describe('CORS Headers for Regular Requests', () => {
185
- it('should add CORS headers for GET requests', async () => {
186
- // Create test data
187
- const doc = createJsonApiDocument('countries', {
188
- name: 'Test Country',
189
- code: 'TC'
190
- })
191
-
192
- const createResult = await api.resources.countries.post({
193
- inputRecord: doc,
194
- simplified: false
195
- })
196
-
197
- // Make HTTP request with origin header
198
- const response = await request(app)
199
- .get('/api/countries')
200
- .set('Origin', 'https://example.com')
201
- .set('Accept', 'application/vnd.api+json')
202
-
203
- assert.equal(response.status, 200)
204
- // When credentials are true, the origin must be specific, not wildcard
205
- assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
206
- assert.equal(response.headers['access-control-allow-credentials'], 'true')
207
- assert.equal(response.headers['access-control-expose-headers'], 'X-Total-Count, X-Page-Count, Link')
208
- // Vary header should be set when specific origin is returned
209
- assert.equal(response.headers['vary'], 'Origin')
210
- })
211
-
212
- it('should add CORS headers for POST requests', async () => {
213
- const doc = createJsonApiDocument('countries', {
214
- name: 'New Country',
215
- code: 'NC'
216
- })
217
-
218
- const response = await request(app)
219
- .post('/api/countries')
220
- .send(doc)
221
- .set('Origin', 'https://example.com')
222
- .set('Content-Type', 'application/vnd.api+json')
223
- .set('Accept', 'application/vnd.api+json')
224
-
225
- assert.equal(response.status, 201)
226
- // When credentials are true, the origin must be specific, not wildcard
227
- assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
228
- assert.equal(response.headers['access-control-allow-credentials'], 'true')
229
- assert.equal(response.headers['access-control-expose-headers'], 'X-Total-Count, X-Page-Count, Link')
230
- // Vary header should be set when specific origin is returned
231
- assert.equal(response.headers['vary'], 'Origin')
232
- })
233
-
234
- it('should handle requests without Origin header', async () => {
235
- // Same-origin request (no Origin header)
236
- const response = await request(app)
237
- .get('/api/countries')
238
- .set('Accept', 'application/vnd.api+json')
239
-
240
- assert.equal(response.status, 200)
241
- // Should not set CORS headers for same-origin requests
242
- assert.equal(response.headers['access-control-allow-origin'], undefined)
243
- })
244
- })
245
-
246
- describe('CORS Preflight Requests', () => {
247
- it('should handle OPTIONS preflight requests', async () => {
248
- const response = await request(app)
249
- .options('/api/countries')
250
- .set('Origin', 'https://example.com')
251
- .set('Access-Control-Request-Method', 'POST')
252
- .set('Access-Control-Request-Headers', 'Content-Type')
253
-
254
- assert.equal(response.status, 204)
255
- // When credentials are true, the origin must be specific, not wildcard
256
- assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
257
- assert.equal(response.headers['access-control-allow-credentials'], 'true')
258
- assert.equal(response.headers['access-control-allow-methods'], 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
259
- assert.equal(response.headers['vary'], 'Origin')
260
- assert.equal(response.headers['access-control-allow-headers'], 'Content-Type, Authorization, X-Custom-Header')
261
- assert.equal(response.headers['access-control-max-age'], '86400')
262
- assert.equal(response.text, '') // Empty body
263
- })
264
-
265
- it('should handle preflight for custom headers', async () => {
266
- const response = await request(app)
267
- .options('/api/countries/1')
268
- .set('Origin', 'https://example.com')
269
- .set('Access-Control-Request-Method', 'PATCH')
270
- .set('Access-Control-Request-Headers', 'X-Custom-Header')
271
-
272
- assert.equal(response.status, 204)
273
- assert(response.headers['access-control-allow-headers'].includes('X-Custom-Header'))
274
- })
275
- })
276
-
277
- describe('CORS with Specific Origins', () => {
278
- it('should work with wildcard origin', async () => {
279
- // Current configuration uses wildcard
280
- const response = await request(app)
281
- .get('/api/countries')
282
- .set('Origin', 'https://any-origin.com')
283
- .set('Accept', 'application/vnd.api+json')
284
-
285
- assert.equal(response.status, 200)
286
- // When credentials are true and origin is sent, specific origin is returned
287
- assert.equal(response.headers['access-control-allow-origin'], 'https://any-origin.com')
288
- assert.equal(response.headers['vary'], 'Origin')
289
- })
290
- })
291
-
292
- describe('CORS Runtime Configuration', () => {
293
- // Tests for dynamic configuration have been removed
294
- // The dynamic configuration API is still available for other tests to use
295
- })
296
-
297
- describe('CORS with Different HTTP Methods', () => {
298
- let testCountryId
299
-
300
- beforeEach(async () => {
301
- // Create test data for update/delete operations
302
- const doc = createJsonApiDocument('countries', {
303
- name: 'Update Test Country',
304
- code: 'UT'
305
- })
306
-
307
- const result = await api.resources.countries.post({
308
- inputRecord: doc,
309
- simplified: false
310
- })
311
-
312
- testCountryId = result.data.id
313
- })
314
-
315
- it('should handle PATCH requests with CORS', async () => {
316
- const patchDoc = {
317
- data: {
318
- type: 'countries',
319
- id: String(testCountryId),
320
- attributes: {
321
- name: 'Updated Country Name'
322
- }
323
- }
324
- }
325
-
326
- const response = await request(app)
327
- .patch(`/api/countries/${testCountryId}`)
328
- .send(patchDoc)
329
- .set('Origin', 'https://example.com')
330
- .set('Content-Type', 'application/vnd.api+json')
331
- .set('Accept', 'application/vnd.api+json')
332
-
333
- assert.equal(response.status, 200)
334
- // When credentials are true and origin is sent, specific origin is returned
335
- assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
336
- assert.equal(response.headers['vary'], 'Origin')
337
- })
338
-
339
- it('should handle DELETE requests with CORS', async () => {
340
- const response = await request(app)
341
- .delete(`/api/countries/${testCountryId}`)
342
- .set('Origin', 'https://example.com')
343
- .set('Accept', 'application/vnd.api+json')
344
-
345
- assert.equal(response.status, 204)
346
- // When credentials are true and origin is sent, specific origin is returned
347
- assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
348
- assert.equal(response.headers['vary'], 'Origin')
349
- })
350
-
351
- it('should handle PUT requests with CORS', async () => {
352
- const putDoc = {
353
- data: {
354
- type: 'countries',
355
- id: testCountryId,
356
- attributes: {
357
- name: 'Completely Replaced Country',
358
- code: 'RC'
359
- }
360
- }
361
- }
362
-
363
- const response = await request(app)
364
- .put(`/api/countries/${testCountryId}`)
365
- .set('Origin', 'https://example.com')
366
- .set('Accept', 'application/vnd.api+json')
367
- .set('Content-Type', 'application/vnd.api+json')
368
- .send(putDoc)
369
-
370
- assert.equal(response.status, 204)
371
- assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
372
- assert.equal(response.headers['vary'], 'Origin')
373
- assert.equal(response.headers['access-control-expose-headers'], 'X-Total-Count, X-Page-Count, Link')
374
- })
375
- })
376
-
377
- describe('CORS with Array and Regex Origins', () => {
378
- it('should support array of allowed origins', async () => {
379
- // Create a new API instance with array origin configuration
380
- const arrayOriginApp = express()
381
- const arrayOriginApi = await createBasicApi(knex, {
382
- apiName: 'cors-array-origins-test',
383
- tablePrefix: 'cors_array',
384
- includeExpress: true,
385
- express: { app: arrayOriginApp }
386
- })
387
-
388
- await arrayOriginApi.use(CorsPlugin, {
389
- origin: ['https://app1.com', 'https://app2.com', 'https://app3.com'],
390
- credentials: true
391
- })
392
-
393
- // Mount routes after CORS plugin is installed
394
- arrayOriginApi.http.express.mount(arrayOriginApp)
395
-
396
- // Test allowed origin
397
- const response1 = await request(arrayOriginApp)
398
- .get('/api/countries')
399
- .set('Origin', 'https://app2.com')
400
- .set('Accept', 'application/vnd.api+json')
401
-
402
- assert.equal(response1.status, 200)
403
- assert.equal(response1.headers['access-control-allow-origin'], 'https://app2.com')
404
-
405
- // Test disallowed origin
406
- const response2 = await request(arrayOriginApp)
407
- .get('/api/countries')
408
- .set('Origin', 'https://app4.com')
409
- .set('Accept', 'application/vnd.api+json')
410
-
411
- assert.equal(response2.status, 200)
412
- assert.equal(response2.headers['access-control-allow-origin'], undefined)
413
- })
414
-
415
- it('should support regex origin matching', async () => {
416
- // Create a new API instance with regex origin configuration
417
- const regexOriginApp = express()
418
- const regexOriginApi = await createBasicApi(knex, {
419
- apiName: 'cors-regex-origins-test',
420
- tablePrefix: 'cors_regex',
421
- includeExpress: true,
422
- express: { app: regexOriginApp }
423
- })
424
-
425
- await regexOriginApi.use(CorsPlugin, {
426
- origin: /^https:\/\/.*\.example\.com$/,
427
- credentials: true
428
- })
429
-
430
- // Mount routes after CORS plugin is installed
431
- regexOriginApi.http.express.mount(regexOriginApp)
432
-
433
- // Test matching subdomain
434
- const response1 = await request(regexOriginApp)
435
- .get('/api/countries')
436
- .set('Origin', 'https://app.example.com')
437
- .set('Accept', 'application/vnd.api+json')
438
-
439
- assert.equal(response1.status, 200)
440
- assert.equal(response1.headers['access-control-allow-origin'], 'https://app.example.com')
441
-
442
- // Test another matching subdomain
443
- const response2 = await request(regexOriginApp)
444
- .get('/api/countries')
445
- .set('Origin', 'https://api.example.com')
446
- .set('Accept', 'application/vnd.api+json')
447
-
448
- assert.equal(response2.status, 200)
449
- assert.equal(response2.headers['access-control-allow-origin'], 'https://api.example.com')
450
-
451
- // Test non-matching domain
452
- const response3 = await request(regexOriginApp)
453
- .get('/api/countries')
454
- .set('Origin', 'https://example.org')
455
- .set('Accept', 'application/vnd.api+json')
456
-
457
- assert.equal(response3.status, 200)
458
- assert.equal(response3.headers['access-control-allow-origin'], undefined)
459
- })
460
-
461
- it('should support function-based origin validation', async () => {
462
- // Create a new API instance with function origin configuration
463
- const functionOriginApp = express()
464
- const functionOriginApi = await createBasicApi(knex, {
465
- apiName: 'cors-function-origins-test',
466
- tablePrefix: 'cors_func',
467
- includeExpress: true,
468
- express: { app: functionOriginApp }
469
- })
470
-
471
- const allowedOrigins = new Set(['https://dynamic1.com', 'https://dynamic2.com'])
472
-
473
- await functionOriginApi.use(CorsPlugin, {
474
- origin: (origin) => allowedOrigins.has(origin),
475
- credentials: true
476
- })
477
-
478
- // Mount routes after CORS plugin is installed
479
- functionOriginApi.http.express.mount(functionOriginApp)
480
-
481
- // Test allowed origin
482
- const response1 = await request(functionOriginApp)
483
- .get('/api/countries')
484
- .set('Origin', 'https://dynamic1.com')
485
- .set('Accept', 'application/vnd.api+json')
486
-
487
- assert.equal(response1.status, 200)
488
- assert.equal(response1.headers['access-control-allow-origin'], 'https://dynamic1.com')
489
-
490
- // Test disallowed origin
491
- const response2 = await request(functionOriginApp)
492
- .get('/api/countries')
493
- .set('Origin', 'https://dynamic3.com')
494
- .set('Accept', 'application/vnd.api+json')
495
-
496
- assert.equal(response2.status, 200)
497
- assert.equal(response2.headers['access-control-allow-origin'], undefined)
498
- })
499
- })
500
-
501
- describe('CORS Error Handling', () => {
502
- it('should reject preflight from disallowed origin', async () => {
503
- // Create a new API instance with specific allowed origin
504
- const restrictedApp = express()
505
- const restrictedApi = await createBasicApi(knex, {
506
- apiName: 'cors-restricted-test',
507
- tablePrefix: 'cors_restrict',
508
- includeExpress: true,
509
- express: { app: restrictedApp }
510
- })
511
-
512
- await restrictedApi.use(CorsPlugin, {
513
- origin: 'https://allowed-only.com',
514
- credentials: true
515
- })
516
-
517
- // Mount routes after CORS plugin is installed
518
- restrictedApi.http.express.mount(restrictedApp)
519
-
520
- const response = await request(restrictedApp)
521
- .options('/api/countries')
522
- .set('Origin', 'https://not-allowed.com')
523
- .set('Access-Control-Request-Method', 'POST')
524
-
525
- assert.equal(response.status, 403)
526
- assert(response.body.error.includes('CORS origin not allowed'))
527
- assert.equal(response.headers['access-control-allow-origin'], undefined)
528
- })
529
-
530
- it('should handle lowercase HTTP methods', async () => {
531
- // Some clients might send lowercase methods
532
- const response = await request(app)
533
- .options('/api/countries')
534
- .set('Origin', 'https://example.com')
535
- .set('Access-Control-Request-Method', 'post') // lowercase
536
-
537
- assert.equal(response.status, 204)
538
- // When credentials are true and origin is sent, specific origin is returned
539
- assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
540
- assert.equal(response.headers['vary'], 'Origin')
541
- })
542
- })
543
-
544
- describe('CORS Integration with REST API', () => {
545
- it('should handle CORS for error responses', async () => {
546
- // Try to get non-existent resource
547
- const response = await request(app)
548
- .get('/api/countries/999999')
549
- .set('Origin', 'https://example.com')
550
- .set('Accept', 'application/vnd.api+json')
551
-
552
- assert.equal(response.status, 404)
553
- // CORS headers should still be set for error responses
554
- assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
555
- assert.equal(response.headers['access-control-allow-credentials'], 'true')
556
- assert.equal(response.headers['vary'], 'Origin')
557
- })
558
-
559
- it('should handle CORS for validation errors', async () => {
560
- // Send invalid data
561
- const invalidDoc = createJsonApiDocument('countries', {
562
- // Missing required 'name' field
563
- code: 'XX'
564
- })
565
-
566
- const response = await request(app)
567
- .post('/api/countries')
568
- .send(invalidDoc)
569
- .set('Origin', 'https://example.com')
570
- .set('Content-Type', 'application/vnd.api+json')
571
- .set('Accept', 'application/vnd.api+json')
572
-
573
- assert.equal(response.status, 422)
574
- // CORS headers should be set for validation errors
575
- assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
576
- assert.equal(response.headers['vary'], 'Origin')
577
- })
578
- })
579
-
580
- describe('CORS Configuration Options', () => {
581
- it('should respect custom max-age configuration', async () => {
582
- // Create API with custom max-age
583
- const maxAgeApp = express()
584
- const maxAgeApi = await createBasicApi(knex, {
585
- apiName: 'cors-maxage-test',
586
- tablePrefix: 'cors_maxage',
587
- includeExpress: true,
588
- express: { app: maxAgeApp }
589
- })
590
-
591
- await maxAgeApi.use(CorsPlugin, {
592
- origin: '*',
593
- credentials: false,
594
- maxAge: 3600 // 1 hour instead of default 24 hours
595
- })
596
-
597
- maxAgeApi.http.express.mount(maxAgeApp)
598
-
599
- const response = await request(maxAgeApp)
600
- .options('/api/countries')
601
- .set('Origin', 'https://example.com')
602
- .set('Access-Control-Request-Method', 'POST')
603
-
604
- assert.equal(response.status, 204)
605
- assert.equal(response.headers['access-control-max-age'], '3600')
606
- })
607
-
608
- it('should respect custom exposed headers configuration', async () => {
609
- // Create API with custom exposed headers
610
- const exposedApp = express()
611
- const exposedApi = await createBasicApi(knex, {
612
- apiName: 'cors-exposed-test',
613
- tablePrefix: 'cors_exposed',
614
- includeExpress: true,
615
- express: { app: exposedApp }
616
- })
617
-
618
- await exposedApi.use(CorsPlugin, {
619
- origin: '*',
620
- credentials: false,
621
- exposedHeaders: ['X-Custom-Header', 'X-Rate-Limit', 'X-Request-Id']
622
- })
623
-
624
- exposedApi.http.express.mount(exposedApp)
625
-
626
- const response = await request(exposedApp)
627
- .get('/api/countries')
628
- .set('Origin', 'https://example.com')
629
- .set('Accept', 'application/vnd.api+json')
630
-
631
- assert.equal(response.status, 200)
632
- assert.equal(response.headers['access-control-expose-headers'], 'X-Custom-Header, X-Rate-Limit, X-Request-Id')
633
- })
634
-
635
- it('should respect custom allowed methods configuration', async () => {
636
- // Create API with limited methods
637
- const methodsApp = express()
638
- const methodsApi = await createBasicApi(knex, {
639
- apiName: 'cors-methods-test',
640
- tablePrefix: 'cors_methods',
641
- includeExpress: true,
642
- express: { app: methodsApp }
643
- })
644
-
645
- await methodsApi.use(CorsPlugin, {
646
- origin: '*',
647
- credentials: false,
648
- methods: ['GET', 'POST'] // Only allow GET and POST
649
- })
650
-
651
- methodsApi.http.express.mount(methodsApp)
652
-
653
- const response = await request(methodsApp)
654
- .options('/api/countries')
655
- .set('Origin', 'https://example.com')
656
- .set('Access-Control-Request-Method', 'DELETE')
657
-
658
- assert.equal(response.status, 204)
659
- assert.equal(response.headers['access-control-allow-methods'], 'GET, POST')
660
- })
661
-
662
- it('should handle credentials false configuration', async () => {
663
- // Create API with credentials disabled
664
- const noCredApp = express()
665
- const noCredApi = await createBasicApi(knex, {
666
- apiName: 'cors-nocred-test',
667
- tablePrefix: 'cors_nocred',
668
- includeExpress: true,
669
- express: { app: noCredApp }
670
- })
671
-
672
- await noCredApi.use(CorsPlugin, {
673
- origin: '*',
674
- credentials: false
675
- })
676
-
677
- noCredApi.http.express.mount(noCredApp)
678
-
679
- const response = await request(noCredApp)
680
- .get('/api/countries')
681
- .set('Origin', 'https://example.com')
682
- .set('Accept', 'application/vnd.api+json')
683
-
684
- assert.equal(response.status, 200)
685
- assert.equal(response.headers['access-control-allow-origin'], '*')
686
- assert.equal(response.headers['access-control-allow-credentials'], undefined)
687
- // No Vary header when using wildcard without credentials
688
- assert.equal(response.headers['vary'], undefined)
689
- })
690
-
691
- it('should handle custom allowed headers configuration', async () => {
692
- // Create API with custom allowed headers
693
- const headersApp = express()
694
- const headersApi = await createBasicApi(knex, {
695
- apiName: 'cors-headers-test',
696
- tablePrefix: 'cors_headers',
697
- includeExpress: true,
698
- express: { app: headersApp }
699
- })
700
-
701
- await headersApi.use(CorsPlugin, {
702
- origin: 'https://app.example.com',
703
- credentials: true,
704
- allowedHeaders: ['Content-Type', 'X-API-Key', 'X-Session-Token']
705
- })
706
-
707
- headersApi.http.express.mount(headersApp)
708
-
709
- const response = await request(headersApp)
710
- .options('/api/countries')
711
- .set('Origin', 'https://app.example.com')
712
- .set('Access-Control-Request-Method', 'POST')
713
- .set('Access-Control-Request-Headers', 'X-API-Key, X-Session-Token')
714
-
715
- assert.equal(response.status, 204)
716
- assert.equal(response.headers['access-control-allow-headers'], 'Content-Type, X-API-Key, X-Session-Token')
717
- })
718
- })
719
-
720
- describe('CORS with PUT Requests', () => {
721
- it('should handle PUT requests with proper CORS headers', async () => {
722
- // The main api already has PUT in its allowed methods
723
- // Create test data first
724
- const doc = createJsonApiDocument('countries', {
725
- name: 'Original Country',
726
- code: 'OC'
727
- })
728
-
729
- const createResult = await api.resources.countries.post({
730
- inputRecord: doc,
731
- simplified: false
732
- })
733
-
734
- const countryId = createResult.data.id
735
-
736
- // Now test PUT with CORS
737
- const putDoc = {
738
- data: {
739
- type: 'countries',
740
- id: String(countryId),
741
- attributes: {
742
- name: 'Replaced Country',
743
- code: 'RC'
744
- }
745
- }
746
- }
747
-
748
- const response = await request(app)
749
- .put(`/api/countries/${countryId}`)
750
- .send(putDoc)
751
- .set('Origin', 'https://example.com')
752
- .set('Content-Type', 'application/vnd.api+json')
753
- .set('Accept', 'application/vnd.api+json')
754
-
755
- assert.equal(response.status, 204)
756
- assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
757
- assert.equal(response.headers['access-control-allow-credentials'], 'true')
758
- assert.equal(response.headers['vary'], 'Origin')
759
-
760
- // Note: PUT might not return the full record by default depending on returnFullRecord settings
761
- // The important part for CORS testing is that the headers are correct, which we've verified above
762
- })
763
-
764
- it('should handle PUT preflight requests', async () => {
765
- const response = await request(app)
766
- .options('/api/countries/123')
767
- .set('Origin', 'https://example.com')
768
- .set('Access-Control-Request-Method', 'PUT')
769
- .set('Access-Control-Request-Headers', 'Content-Type')
770
-
771
- assert.equal(response.status, 204)
772
- assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
773
- assert.equal(response.headers['access-control-allow-methods'], 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
774
- assert(response.headers['access-control-allow-methods'].includes('PUT'))
775
- })
776
- })
777
-
778
- describe('CORS Edge Cases', () => {
779
- it('should handle requests with no exposed headers configured', async () => {
780
- // Create API with empty exposed headers
781
- const noExposedApp = express()
782
- const noExposedApi = await createBasicApi(knex, {
783
- apiName: 'cors-noexposed-test',
784
- tablePrefix: 'cors_noexposed',
785
- includeExpress: true,
786
- express: { app: noExposedApp }
787
- })
788
-
789
- await noExposedApi.use(CorsPlugin, {
790
- origin: '*',
791
- credentials: false,
792
- exposedHeaders: [] // Empty array
793
- })
794
-
795
- noExposedApi.http.express.mount(noExposedApp)
796
-
797
- const response = await request(noExposedApp)
798
- .get('/api/countries')
799
- .set('Origin', 'https://example.com')
800
- .set('Accept', 'application/vnd.api+json')
801
-
802
- assert.equal(response.status, 200)
803
- // Should not set exposed headers when array is empty
804
- assert.equal(response.headers['access-control-expose-headers'], undefined)
805
- })
806
-
807
- it('should handle optionsSuccessStatus configuration', async () => {
808
- // Create API with custom options success status
809
- const statusApp = express()
810
- const statusApi = await createBasicApi(knex, {
811
- apiName: 'cors-status-test',
812
- tablePrefix: 'cors_status',
813
- includeExpress: true,
814
- express: { app: statusApp }
815
- })
816
-
817
- await statusApi.use(CorsPlugin, {
818
- origin: '*',
819
- credentials: false,
820
- optionsSuccessStatus: 200 // Use 200 instead of 204
821
- })
822
-
823
- statusApi.http.express.mount(statusApp)
824
-
825
- const response = await request(statusApp)
826
- .options('/api/countries')
827
- .set('Origin', 'https://example.com')
828
- .set('Access-Control-Request-Method', 'GET')
829
-
830
- assert.equal(response.status, 200)
831
- assert.equal(response.headers['access-control-allow-origin'], '*')
832
- })
833
- })
834
- })