json-rest-api 1.0.29 → 2.1.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 (294) 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 +733 -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 +103 -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 +3838 -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 +37 -0
  44. package/index.js +3 -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 +70 -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 +586 -418
  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 +120 -56
  71. package/plugins/core/lib/anyapi/stored-definition.js +109 -0
  72. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  73. package/plugins/core/lib/dbIntrospection.js +228 -21
  74. package/plugins/core/lib/dbTablesOperations.js +409 -92
  75. package/plugins/core/lib/fractional-positioning.js +12 -48
  76. package/plugins/core/lib/querying/identifier-query.js +19 -0
  77. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  78. package/plugins/core/lib/querying/include-to-many.js +428 -0
  79. package/plugins/core/lib/querying/include-to-one.js +579 -0
  80. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  81. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  82. package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
  83. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  84. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  85. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  86. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  87. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  88. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  89. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  90. package/plugins/core/lib/querying/query-constraint.js +27 -0
  91. package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
  92. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  93. package/plugins/core/lib/querying/sort-helpers.js +13 -1
  94. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  95. package/plugins/core/lib/querying/url-helpers.js +11 -4
  96. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  97. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  98. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  99. package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
  100. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  101. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  102. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  103. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  104. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  105. package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
  106. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  107. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  108. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  109. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  110. package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
  111. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  112. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  113. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  114. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  115. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  116. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  117. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  118. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  119. package/plugins/core/lib/storage/storage-adapter.js +128 -62
  120. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  121. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  122. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  123. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  124. package/plugins/core/lib/writing/resource-version.js +172 -0
  125. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  126. package/plugins/core/rest-api-anyapi-knex-plugin.js +290 -2475
  127. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  128. package/plugins/core/rest-api-cors-plugin.js +52 -205
  129. package/plugins/core/rest-api-knex-plugin.js +45 -1077
  130. package/plugins/core/rest-api-label-plugin.js +25 -38
  131. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  132. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  133. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  134. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  135. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  136. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  137. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  138. package/plugins/core/rest-api-plugin-methods/common.js +291 -395
  139. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  140. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  141. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
  142. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  143. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  144. package/plugins/core/rest-api-plugin-methods/get.js +36 -68
  145. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  146. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  147. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  148. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  149. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  150. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  151. package/plugins/core/rest-api-plugin-methods/query.js +37 -81
  152. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  153. package/plugins/core/rest-api-plugin.js +32 -46
  154. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  155. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  156. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  157. package/plugins/core/socketio-plugin.js +364 -344
  158. package/plugins/storage/local-storage.js +40 -36
  159. package/types/anyapi.d.ts +30 -0
  160. package/types/autofilter.d.ts +57 -0
  161. package/types/bulk-methods.d.ts +63 -0
  162. package/types/errors.d.ts +118 -0
  163. package/types/fastify-options.d.ts +19 -0
  164. package/types/file-detectors.d.ts +33 -0
  165. package/types/file-storage.d.ts +48 -0
  166. package/types/hook-context.d.ts +180 -0
  167. package/types/plugin-options.d.ts +65 -0
  168. package/types/relationship-methods.d.ts +74 -0
  169. package/types/representations.d.ts +50 -0
  170. package/types/resource-methods.d.ts +95 -0
  171. package/types/resource-schema.d.ts +157 -0
  172. package/types/row-policy.d.ts +31 -0
  173. package/types/runtime.d.ts +132 -0
  174. package/types/transactions.d.ts +21 -0
  175. package/.claude/agents/library-guide-expert.md +0 -30
  176. package/.claude/agents/rest-api-internals-expert.md +0 -54
  177. package/.claude/settings.local.json +0 -70
  178. package/AGENTS.md +0 -34
  179. package/CLAUDE.md +0 -160
  180. package/CRUSH.md +0 -55
  181. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  182. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  183. package/TODO.todo +0 -13
  184. package/connectors_ideas/fastify.js +0 -217
  185. package/connectors_ideas/koa-plugin.js +0 -112
  186. package/docs/.bundle/config +0 -2
  187. package/docs/COMPARISON.md +0 -433
  188. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  189. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  190. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  191. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  192. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  193. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  194. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  195. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
  196. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  197. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  198. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  199. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  200. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  201. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  202. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  203. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  204. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  205. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  206. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  207. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  208. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  209. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  210. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  211. package/docs/Gemfile +0 -6
  212. package/docs/Gemfile.lock +0 -308
  213. package/docs/ONBOARDING.md +0 -315
  214. package/docs/_config.yml +0 -36
  215. package/docs/_layouts/default.html +0 -123
  216. package/docs/apple-touch-icon.svg +0 -4
  217. package/docs/assets/css/style.scss +0 -392
  218. package/docs/favicon.svg +0 -4
  219. package/docs/index.md +0 -164
  220. package/docs/mini-guides/file-uploads-complete.md +0 -161
  221. package/eslint.config.js +0 -13
  222. package/examples/url-override-example.js +0 -137
  223. package/fixing_plan.md +0 -112
  224. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  225. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  226. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  227. package/plugins/storage/s3-storage.js +0 -93
  228. package/quickTest.js +0 -136
  229. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  230. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  231. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  232. package/scripts/run-docs.js +0 -133
  233. package/test-debug.js +0 -59
  234. package/tests/README.md +0 -202
  235. package/tests/TEST_TEMPLATE.test.js +0 -178
  236. package/tests/anyapi-basic.test.js +0 -105
  237. package/tests/anyapi-cursor-pagination.test.js +0 -148
  238. package/tests/anyapi-custom-idproperty.test.js +0 -163
  239. package/tests/autofilter.test.js +0 -463
  240. package/tests/bulk-operations.test.js +0 -657
  241. package/tests/computed-fields.test.js +0 -586
  242. package/tests/cors.test.js +0 -834
  243. package/tests/curl.test.js +0 -467
  244. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  245. package/tests/custom-id-simplified-post.test.js +0 -75
  246. package/tests/custom-idproperty-relationships.test.js +0 -932
  247. package/tests/db-introspection.test.js +0 -521
  248. package/tests/db-table-operations.test.js +0 -798
  249. package/tests/fastify-plugin.test.js +0 -184
  250. package/tests/field-getters.test.js +0 -370
  251. package/tests/field-setters.test.js +0 -306
  252. package/tests/file-handling.test.js +0 -155
  253. package/tests/fixtures/api-configs.js +0 -2566
  254. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  255. package/tests/helpers/fake-fastify.js +0 -99
  256. package/tests/helpers/storage-mode.js +0 -65
  257. package/tests/helpers/test-utils.js +0 -203
  258. package/tests/http-connectors-parity.test.js +0 -395
  259. package/tests/id-normalization.test.js +0 -444
  260. package/tests/include-depth-validation.test.js +0 -331
  261. package/tests/includes.test.js +0 -427
  262. package/tests/local-storage.test.js +0 -96
  263. package/tests/logical-id-write-validation.test.js +0 -151
  264. package/tests/nested-includes.test.js +0 -617
  265. package/tests/pagination-boundaries.test.js +0 -154
  266. package/tests/pagination-contract.test.js +0 -122
  267. package/tests/pagination-cursor-multifield.test.js +0 -641
  268. package/tests/pagination-enhanced.test.js +0 -563
  269. package/tests/polymorphic-relationships.test.js +0 -347
  270. package/tests/polymorphic-simple.test.js +0 -148
  271. package/tests/positioning-advanced.test.js +0 -692
  272. package/tests/positioning-extended.test.js +0 -915
  273. package/tests/positioning-multifilter.test.js +0 -699
  274. package/tests/positioning.test.js +0 -560
  275. package/tests/post-commit-hooks.test.js +0 -120
  276. package/tests/projected-fields.test.js +0 -319
  277. package/tests/public-exports.test.js +0 -15
  278. package/tests/put-create-parity.test.js +0 -216
  279. package/tests/queries.test.js +0 -521
  280. package/tests/query-limits.test.js +0 -584
  281. package/tests/relationship-endpoints.test.js +0 -620
  282. package/tests/relationship-include-storage-adapter.test.js +0 -56
  283. package/tests/relationships.test.js +0 -681
  284. package/tests/rest-api.test.js +0 -337
  285. package/tests/return-record-settings.test.js +0 -184
  286. package/tests/row-policy.test.js +0 -489
  287. package/tests/s3-storage.test.js +0 -38
  288. package/tests/searchschema-knex-behavior.test.js +0 -187
  289. package/tests/searchschema-merge.test.js +0 -151
  290. package/tests/socketio.test.js +0 -860
  291. package/tests/storage-mapping.test.js +0 -362
  292. package/tests/temporal-boundaries.test.js +0 -208
  293. package/tests/test-development-guide.md +0 -370
  294. package/tests/virtual-fields.test.js +0 -404
@@ -1,467 +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 { spawn } from 'child_process'
5
- import { createBasicApi } from './fixtures/api-configs.js'
6
- import {
7
- validateJsonApiStructure,
8
- resourceIdentifier,
9
- cleanTables,
10
- createJsonApiDocument,
11
- createRelationship,
12
- createToManyRelationship
13
- } from './helpers/test-utils.js'
14
- import express from 'express'
15
- import http from 'http'
16
-
17
- // Create Knex instance for tests
18
- const knex = knexLib({
19
- client: 'better-sqlite3',
20
- connection: {
21
- filename: ':memory:'
22
- },
23
- useNullAsDefault: true
24
- })
25
-
26
- // API instances that persist across tests
27
- let basicApi
28
- let app
29
- let server
30
- const TEST_PORT = 3456
31
-
32
- /**
33
- * Execute a CURL command and return the response
34
- * @param {string[]} args - The curl command arguments
35
- * @returns {Promise<{stdout: string, stderr: string, exitCode: number}>}
36
- */
37
- async function executeCurl (args) {
38
- return new Promise((resolve, reject) => {
39
- const curl = spawn('curl', args, {
40
- shell: false
41
- })
42
-
43
- let stdout = ''
44
- let stderr = ''
45
-
46
- curl.stdout.on('data', (data) => {
47
- stdout += data.toString()
48
- })
49
-
50
- curl.stderr.on('data', (data) => {
51
- stderr += data.toString()
52
- })
53
-
54
- curl.on('close', (code) => {
55
- resolve({
56
- stdout,
57
- stderr,
58
- exitCode: code
59
- })
60
- })
61
-
62
- curl.on('error', (error) => {
63
- reject(error)
64
- })
65
- })
66
- }
67
-
68
- describe('CURL HTTP Abstraction Layer Tests', () => {
69
- before(async () => {
70
- // Initialize API with Express plugin
71
- basicApi = await createBasicApi(knex, { includeExpress: true })
72
-
73
- // Create Express app
74
- app = express()
75
-
76
- // Mount the API routes
77
- basicApi.http.express.mount(app, '')
78
-
79
- // Start server
80
- server = http.createServer(app)
81
- await new Promise((resolve) => {
82
- server.listen(TEST_PORT, () => {
83
- console.log(`Test server started on port ${TEST_PORT}`)
84
- resolve()
85
- })
86
- })
87
- })
88
-
89
- after(async () => {
90
- // Close server
91
- await new Promise((resolve) => {
92
- server.close(() => {
93
- console.log('Test server closed')
94
- resolve()
95
- })
96
- })
97
-
98
- // Close database connection
99
- await knex.destroy()
100
- })
101
-
102
- describe('Basic CURL Operations', () => {
103
- const testData = {}
104
-
105
- beforeEach(async () => {
106
- // Clean all tables
107
- await cleanTables(knex, [
108
- 'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors'
109
- ])
110
-
111
- // Create test data using API methods
112
- const countryDoc = createJsonApiDocument('countries', { name: 'CURL Test Country', code: 'CT' })
113
- const countryResult = await basicApi.resources.countries.post({
114
- inputRecord: countryDoc,
115
- simplified: false
116
- })
117
- testData.country = countryResult.data
118
- })
119
-
120
- it('should GET a resource using CURL', async () => {
121
- const result = await executeCurl(['-s', '-H', 'Accept: application/vnd.api+json', `http://localhost:${TEST_PORT}/api/countries/${testData.country.id}`])
122
-
123
- assert.equal(result.exitCode, 0, 'CURL should exit successfully')
124
-
125
- const response = JSON.parse(result.stdout)
126
- validateJsonApiStructure(response, false)
127
-
128
- assert.equal(response.data.type, 'countries')
129
- assert.equal(response.data.id, testData.country.id)
130
- assert.equal(response.data.attributes.name, 'CURL Test Country')
131
- assert.equal(response.data.attributes.code, 'CT')
132
- })
133
-
134
- it('should GET collection using CURL', async () => {
135
- const result = await executeCurl(['-s', '-H', 'Accept: application/vnd.api+json', `http://localhost:${TEST_PORT}/api/countries`])
136
-
137
- assert.equal(result.exitCode, 0, 'CURL should exit successfully')
138
-
139
- const response = JSON.parse(result.stdout)
140
- validateJsonApiStructure(response, true)
141
-
142
- assert(Array.isArray(response.data), 'Response data should be an array')
143
- assert.equal(response.data.length, 1, 'Should have one country')
144
- assert.equal(response.data[0].type, 'countries')
145
- assert.equal(response.data[0].attributes.name, 'CURL Test Country')
146
- })
147
-
148
- it('should POST a new resource using CURL', async () => {
149
- const newCountryDoc = createJsonApiDocument('countries', { name: 'New CURL Country', code: 'NC' })
150
- const jsonPayload = JSON.stringify(newCountryDoc)
151
-
152
- const result = await executeCurl(['-s', '-X', 'POST', '-H', 'Content-Type: application/vnd.api+json', '-H', 'Accept: application/vnd.api+json', '-d', jsonPayload, `http://localhost:${TEST_PORT}/api/countries`])
153
-
154
- assert.equal(result.exitCode, 0, 'CURL should exit successfully')
155
-
156
- const response = JSON.parse(result.stdout)
157
- validateJsonApiStructure(response, false)
158
-
159
- assert.equal(response.data.type, 'countries')
160
- assert(response.data.id, 'Should have an ID')
161
- assert.equal(response.data.attributes.name, 'New CURL Country')
162
- assert.equal(response.data.attributes.code, 'NC')
163
- })
164
-
165
- it('should PATCH a resource using CURL', async () => {
166
- const patchDoc = {
167
- data: {
168
- type: 'countries',
169
- id: String(testData.country.id),
170
- attributes: {
171
- name: 'Updated CURL Country'
172
- }
173
- }
174
- }
175
- const jsonPayload = JSON.stringify(patchDoc)
176
-
177
- const result = await executeCurl(['-s', '-X', 'PATCH', '-H', 'Content-Type: application/vnd.api+json', '-H', 'Accept: application/vnd.api+json', '-d', jsonPayload, `http://localhost:${TEST_PORT}/api/countries/${testData.country.id}`])
178
-
179
- assert.equal(result.exitCode, 0, 'CURL should exit successfully')
180
-
181
- const response = JSON.parse(result.stdout)
182
-
183
- // PATCH is configured to return minimal JSON:API response
184
- assert(response.data, 'Should have data')
185
- assert.equal(response.data.type, 'countries', 'Should have correct type')
186
- assert.equal(response.data.id, String(testData.country.id), 'Should return correct ID')
187
-
188
- // Verify the update by fetching the record
189
- const verifyResult = await basicApi.resources.countries.get({
190
- id: testData.country.id,
191
- simplified: false
192
- })
193
- assert.equal(verifyResult.data.attributes.name, 'Updated CURL Country')
194
- assert.equal(verifyResult.data.attributes.code, 'CT', 'Code should remain unchanged')
195
- })
196
-
197
- it('should DELETE a resource using CURL', async () => {
198
- const result = await executeCurl(['-s', '-X', 'DELETE', '-w', '%{http_code}', '-o', '/dev/null', `http://localhost:${TEST_PORT}/api/countries/${testData.country.id}`])
199
-
200
- assert.equal(result.exitCode, 0, 'CURL should exit successfully')
201
- assert.equal(result.stdout.trim(), '204', 'Should return 204 No Content')
202
-
203
- // Verify deletion
204
- try {
205
- await basicApi.resources.countries.get({
206
- id: testData.country.id,
207
- simplified: false
208
- })
209
- assert.fail('Should have thrown not found error')
210
- } catch (error) {
211
- assert.equal(error.code, 'REST_API_RESOURCE')
212
- assert.equal(error.subtype, 'not_found')
213
- }
214
- })
215
-
216
- it('should handle query parameters using CURL', async () => {
217
- // Create additional countries
218
- await basicApi.resources.countries.post({
219
- inputRecord: createJsonApiDocument('countries', { name: 'Another Country', code: 'AC' }),
220
- simplified: false
221
- })
222
- await basicApi.resources.countries.post({
223
- inputRecord: createJsonApiDocument('countries', { name: 'Third Country', code: 'TC2' }),
224
- simplified: false
225
- })
226
-
227
- // Test with filter (URL encode the space and square brackets)
228
- const result = await executeCurl(['-s', '-H', 'Accept: application/vnd.api+json', `http://localhost:${TEST_PORT}/api/countries?filter%5Bname%5D=Another%20Country`])
229
-
230
- assert.equal(result.exitCode, 0, 'CURL should exit successfully')
231
-
232
- const response = JSON.parse(result.stdout)
233
- validateJsonApiStructure(response, true)
234
-
235
- assert.equal(response.data.length, 1, 'Should filter to one country')
236
- assert.equal(response.data[0].attributes.name, 'Another Country')
237
- })
238
-
239
- it('should handle includes using CURL', async () => {
240
- // Create publisher with country relationship
241
- const publisherDoc = createJsonApiDocument('publishers',
242
- { name: 'CURL Test Publisher' },
243
- { country: createRelationship(resourceIdentifier('countries', testData.country.id)) }
244
- )
245
- const publisherResult = await basicApi.resources.publishers.post({
246
- inputRecord: publisherDoc,
247
- simplified: false
248
- })
249
-
250
- const result = await executeCurl(['-s', '-H', 'Accept: application/vnd.api+json', `http://localhost:${TEST_PORT}/api/publishers/${publisherResult.data.id}?include=country`])
251
-
252
- assert.equal(result.exitCode, 0, 'CURL should exit successfully')
253
-
254
- const response = JSON.parse(result.stdout)
255
- validateJsonApiStructure(response, false)
256
-
257
- assert.equal(response.data.type, 'publishers')
258
- assert(response.included, 'Should have included data')
259
- assert.equal(response.included.length, 1, 'Should include one resource')
260
- assert.equal(response.included[0].type, 'countries')
261
- assert.equal(response.included[0].id, testData.country.id)
262
- })
263
-
264
- it('should handle errors using CURL', async () => {
265
- // Test 404 error
266
- const result404 = await executeCurl(['-s', '-w', '\\n%{http_code}', '-H', 'Accept: application/vnd.api+json', `http://localhost:${TEST_PORT}/api/countries/999999`])
267
-
268
- assert.equal(result404.exitCode, 0, 'CURL should exit successfully')
269
-
270
- const lines = result404.stdout.trim().split('\n')
271
- const httpCode = lines[lines.length - 1]
272
- const responseBody = lines.slice(0, -1).join('\n')
273
-
274
- assert.equal(httpCode, '404', 'Should return 404 status')
275
-
276
- const response = JSON.parse(responseBody)
277
- assert(response.errors, 'Should have errors array')
278
- assert.equal(response.errors[0].status, '404')
279
- assert.equal(response.errors[0].title, 'Not Found')
280
-
281
- // Test validation error - omit required name field
282
- const invalidDoc = createJsonApiDocument('countries', { code: 'XX' }) // Missing required name
283
- const jsonPayload = JSON.stringify(invalidDoc)
284
-
285
- const resultValidation = await executeCurl(['-s', '-w', '\n%{http_code}', '-X', 'POST', '-H', 'Content-Type: application/vnd.api+json', '-H', 'Accept: application/vnd.api+json', '-d', jsonPayload, `http://localhost:${TEST_PORT}/api/countries`])
286
-
287
- const validationLines = resultValidation.stdout.trim().split('\n')
288
- const validationCode = validationLines[validationLines.length - 1]
289
- const validationBody = validationLines.slice(0, -1).join('\n')
290
-
291
- assert.equal(validationCode, '422', 'Should return 422 for validation error')
292
-
293
- const validationResponse = JSON.parse(validationBody)
294
- assert(validationResponse.errors, 'Should have errors array')
295
- assert.equal(validationResponse.errors[0].status, '422')
296
- })
297
-
298
- it('should handle different content types using CURL', async () => {
299
- // Test with application/json content type
300
- const newCountryDoc = createJsonApiDocument('countries', { name: 'JSON Country', code: 'JC' })
301
- const jsonPayload = JSON.stringify(newCountryDoc)
302
-
303
- const resultJson = await executeCurl(['-s', '-X', 'POST', '-H', 'Content-Type: application/json', '-H', 'Accept: application/vnd.api+json', '-d', jsonPayload, `http://localhost:${TEST_PORT}/api/countries`])
304
-
305
- assert.equal(resultJson.exitCode, 0, 'CURL should exit successfully with application/json')
306
-
307
- const responseJson = JSON.parse(resultJson.stdout)
308
- validateJsonApiStructure(responseJson, false)
309
- assert.equal(responseJson.data.attributes.name, 'JSON Country')
310
-
311
- // Test with unsupported content type (should fail if strictContentType is enabled)
312
- const resultXml = await executeCurl(['-s', '-w', '\n%{http_code}', '-X', 'POST', '-H', 'Content-Type: application/xml', '-H', 'Accept: application/vnd.api+json', '-d', jsonPayload, `http://localhost:${TEST_PORT}/api/countries`])
313
-
314
- const xmlLines = resultXml.stdout.trim().split('\n')
315
- const xmlCode = xmlLines[xmlLines.length - 1]
316
-
317
- assert.equal(xmlCode, '415', 'Should return 415 Unsupported Media Type')
318
- })
319
-
320
- it('should handle headers correctly using CURL', async () => {
321
- // Test with custom headers (-v outputs to stderr)
322
- const result = await executeCurl(['-s', '-H', 'Accept: application/vnd.api+json', '-H', 'X-Custom-Header: test-value', '-H', 'User-Agent: CURL-Test/1.0', '-v', `http://localhost:${TEST_PORT}/api/countries/${testData.country.id}`])
323
-
324
- assert.equal(result.exitCode, 0, 'CURL should exit successfully')
325
-
326
- // Check that headers were sent (visible in verbose output on stderr)
327
- assert(result.stderr.includes('X-Custom-Header: test-value'), 'Should send custom header')
328
- assert(result.stderr.includes('User-Agent: CURL-Test/1.0'), 'Should send custom user agent')
329
- })
330
- })
331
-
332
- describe('Complex CURL Operations', () => {
333
- const testData = {}
334
-
335
- beforeEach(async () => {
336
- await cleanTables(knex, [
337
- 'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors'
338
- ])
339
-
340
- // Create complex test data
341
- const countryDoc = createJsonApiDocument('countries', { name: 'Complex Country', code: 'CC' })
342
- const countryResult = await basicApi.resources.countries.post({
343
- inputRecord: countryDoc,
344
- simplified: false
345
- })
346
- testData.country = countryResult.data
347
-
348
- const publisherDoc = createJsonApiDocument('publishers',
349
- { name: 'Complex Publisher' },
350
- { country: createRelationship(resourceIdentifier('countries', testData.country.id)) }
351
- )
352
- const publisherResult = await basicApi.resources.publishers.post({
353
- inputRecord: publisherDoc,
354
- simplified: false
355
- })
356
- testData.publisher = publisherResult.data
357
-
358
- const author1Doc = createJsonApiDocument('authors', { name: 'Complex Author One' })
359
- const author2Doc = createJsonApiDocument('authors', { name: 'Complex Author Two' })
360
- const author1Result = await basicApi.resources.authors.post({
361
- inputRecord: author1Doc,
362
- simplified: false
363
- })
364
- const author2Result = await basicApi.resources.authors.post({
365
- inputRecord: author2Doc,
366
- simplified: false
367
- })
368
- testData.authors = [author1Result.data, author2Result.data]
369
-
370
- const bookDoc = createJsonApiDocument('books',
371
- { title: 'Complex Book' },
372
- {
373
- country: createRelationship(resourceIdentifier('countries', testData.country.id)),
374
- publisher: createRelationship(resourceIdentifier('publishers', testData.publisher.id)),
375
- authors: createToManyRelationship([
376
- resourceIdentifier('authors', testData.authors[0].id),
377
- resourceIdentifier('authors', testData.authors[1].id)
378
- ])
379
- }
380
- )
381
- const bookResult = await basicApi.resources.books.post({
382
- inputRecord: bookDoc,
383
- simplified: false
384
- })
385
- testData.book = bookResult.data
386
- })
387
-
388
- it('should handle nested includes using CURL', async () => {
389
- const result = await executeCurl(['-s', '-H', 'Accept: application/vnd.api+json', `http://localhost:${TEST_PORT}/api/books/${testData.book.id}?include=publisher.country,authors`])
390
-
391
- assert.equal(result.exitCode, 0, 'CURL should exit successfully')
392
-
393
- const response = JSON.parse(result.stdout)
394
- validateJsonApiStructure(response, false)
395
-
396
- assert.equal(response.data.type, 'books')
397
- assert(response.included, 'Should have included data')
398
-
399
- // Should include publisher, country, and 2 authors
400
- assert.equal(response.included.length, 4, 'Should include 4 resources')
401
-
402
- const includedTypes = response.included.map(r => r.type)
403
- assert(includedTypes.includes('publishers'), 'Should include publisher')
404
- assert(includedTypes.includes('countries'), 'Should include country')
405
- assert.equal(includedTypes.filter(t => t === 'authors').length, 2, 'Should include 2 authors')
406
- })
407
-
408
- it('should handle pagination using CURL', async () => {
409
- // Create multiple books
410
- for (let i = 0; i < 5; i++) {
411
- await basicApi.resources.books.post({
412
- inputRecord: createJsonApiDocument('books',
413
- { title: `Book ${i}` },
414
- {
415
- country: createRelationship(resourceIdentifier('countries', testData.country.id)),
416
- publisher: createRelationship(resourceIdentifier('publishers', testData.publisher.id))
417
- }
418
- ),
419
- simplified: false
420
- })
421
- }
422
-
423
- // Test pagination (URL encode square brackets)
424
- const result = await executeCurl(['-s', '-H', 'Accept: application/vnd.api+json', `http://localhost:${TEST_PORT}/api/books?page%5Bsize%5D=3&page%5Bnumber%5D=1`])
425
-
426
- assert.equal(result.exitCode, 0, 'CURL should exit successfully')
427
-
428
- const response = JSON.parse(result.stdout)
429
- validateJsonApiStructure(response, true)
430
-
431
- assert.equal(response.data.length, 3, 'Should return 3 books per page')
432
- // Note: Links are now always included, using relative URLs whenreturnBasePath isn't configured
433
- })
434
-
435
- it('should handle sorting using CURL', async () => {
436
- const result = await executeCurl(['-s', '-H', 'Accept: application/vnd.api+json', `http://localhost:${TEST_PORT}/api/books?sort=-title`])
437
-
438
- assert.equal(result.exitCode, 0, 'CURL should exit successfully')
439
-
440
- const response = JSON.parse(result.stdout)
441
- validateJsonApiStructure(response, true)
442
-
443
- // Books should be sorted by title descending
444
- const titles = response.data.map(book => book.attributes.title)
445
- const sortedTitles = [...titles].sort().reverse()
446
- assert.deepEqual(titles, sortedTitles, 'Books should be sorted by title descending')
447
- })
448
-
449
- it('should handle sparse fieldsets using CURL', async () => {
450
- const result = await executeCurl(['-s', '-H', 'Accept: application/vnd.api+json', `http://localhost:${TEST_PORT}/api/books/${testData.book.id}?fields%5Bbooks%5D=title&fields%5Bpublishers%5D=name&include=publisher`])
451
-
452
- assert.equal(result.exitCode, 0, 'CURL should exit successfully')
453
-
454
- const response = JSON.parse(result.stdout)
455
- validateJsonApiStructure(response, false)
456
-
457
- // Main resource should only have title attribute
458
- assert.equal(Object.keys(response.data.attributes).length, 1, 'Should only have title attribute')
459
- assert(response.data.attributes.title, 'Should have title')
460
-
461
- // Included publisher should only have name attribute
462
- const publisher = response.included.find(r => r.type === 'publishers')
463
- assert.equal(Object.keys(publisher.attributes).length, 1, 'Publisher should only have name attribute')
464
- assert(publisher.attributes.name, 'Publisher should have name')
465
- })
466
- })
467
- })
@@ -1,128 +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 { Api } from 'hooked-api'
5
- import { RestApiPlugin } from '../plugins/core/rest-api-plugin.js'
6
- import { RestApiKnexPlugin } from '../plugins/core/rest-api-knex-plugin.js'
7
- import {
8
- cleanTables,
9
- validateJsonApiStructure
10
- } from './helpers/test-utils.js'
11
-
12
- const knex = knexLib({
13
- client: 'better-sqlite3',
14
- connection: {
15
- filename: ':memory:'
16
- },
17
- useNullAsDefault: true
18
- })
19
-
20
- let api
21
-
22
- function createMysqlStyleInsertDb (baseKnex) {
23
- return (tableName) => {
24
- const query = baseKnex(tableName)
25
- const originalInsert = query.insert.bind(query)
26
-
27
- query.insert = (...args) => {
28
- const insertBuilder = originalInsert(...args)
29
- const originalReturning = insertBuilder.returning.bind(insertBuilder)
30
-
31
- insertBuilder.returning = async (...returnArgs) => {
32
- await originalReturning(...returnArgs)
33
- return [0]
34
- }
35
-
36
- return insertBuilder
37
- }
38
-
39
- return query
40
- }
41
- }
42
-
43
- describe('Custom id POST fallback when insert returning is unusable', () => {
44
- before(async () => {
45
- api = new Api({
46
- name: 'custom-id-mysql-returning-fallback-test'
47
- })
48
-
49
- await api.use(RestApiPlugin, {
50
- simplifiedApi: true,
51
- simplifiedTransport: false,
52
- returnRecordApi: {
53
- post: 'full',
54
- put: 'full',
55
- patch: 'full'
56
- }
57
- })
58
- await api.use(RestApiKnexPlugin, { knex })
59
-
60
- await api.addResource('user_settings', {
61
- tableName: 'mysqlish_user_settings',
62
- idProperty: 'user_id',
63
- searchSchema: {
64
- id: { type: 'id', actualField: 'id' }
65
- },
66
- schema: {
67
- theme: { type: 'string', required: true, max: 32 }
68
- }
69
- })
70
- await api.resources.user_settings.createKnexTable()
71
- })
72
-
73
- after(async () => {
74
- await knex.destroy()
75
- })
76
-
77
- beforeEach(async () => {
78
- await cleanTables(knex, ['mysqlish_user_settings'])
79
- })
80
-
81
- it('returns the explicit id for simplified POST when the insert result is 0', async () => {
82
- const mysqlStyleDb = createMysqlStyleInsertDb(knex)
83
-
84
- const created = await api.resources.user_settings.post({
85
- inputRecord: {
86
- id: '6',
87
- theme: 'dark'
88
- },
89
- simplified: true,
90
- transaction: mysqlStyleDb
91
- })
92
-
93
- assert.equal(created.id, '6')
94
- assert.equal(created.theme, 'dark')
95
-
96
- const dbRow = await knex('mysqlish_user_settings').where('user_id', 6).first()
97
- assert.ok(dbRow)
98
- assert.equal(String(dbRow.user_id), '6')
99
- assert.equal(dbRow.theme, 'dark')
100
- })
101
-
102
- it('returns the explicit id for non-simplified POST when the insert result is 0', async () => {
103
- const mysqlStyleDb = createMysqlStyleInsertDb(knex)
104
-
105
- const created = await api.resources.user_settings.post({
106
- inputRecord: {
107
- data: {
108
- type: 'user_settings',
109
- id: '8',
110
- attributes: {
111
- theme: 'light'
112
- }
113
- }
114
- },
115
- simplified: false,
116
- transaction: mysqlStyleDb
117
- })
118
-
119
- validateJsonApiStructure(created)
120
- assert.equal(created.data.id, '8')
121
- assert.equal(created.data.attributes.theme, 'light')
122
-
123
- const dbRow = await knex('mysqlish_user_settings').where('user_id', 8).first()
124
- assert.ok(dbRow)
125
- assert.equal(String(dbRow.user_id), '8')
126
- assert.equal(dbRow.theme, 'light')
127
- })
128
- })
@@ -1,75 +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 { Api } from 'hooked-api'
5
- import { RestApiPlugin } from '../plugins/core/rest-api-plugin.js'
6
- import { RestApiKnexPlugin } from '../plugins/core/rest-api-knex-plugin.js'
7
- import { cleanTables } from './helpers/test-utils.js'
8
-
9
- const knex = knexLib({
10
- client: 'better-sqlite3',
11
- connection: {
12
- filename: ':memory:'
13
- },
14
- useNullAsDefault: true
15
- })
16
-
17
- let api
18
-
19
- describe('Simplified POST with explicit resource ids', () => {
20
- before(async () => {
21
- api = new Api({
22
- name: 'custom-id-simplified-post-test'
23
- })
24
-
25
- await api.use(RestApiPlugin, {
26
- simplifiedApi: true,
27
- simplifiedTransport: false,
28
- returnRecordApi: {
29
- post: 'full',
30
- put: 'full',
31
- patch: 'full'
32
- }
33
- })
34
- await api.use(RestApiKnexPlugin, { knex })
35
-
36
- await api.addResource('user_settings', {
37
- tableName: 'test_user_settings',
38
- idProperty: 'user_id',
39
- searchSchema: {
40
- id: { type: 'id', actualField: 'id' }
41
- },
42
- schema: {
43
- theme: { type: 'string', required: true, max: 32 }
44
- }
45
- })
46
- await api.resources.user_settings.createKnexTable()
47
- })
48
-
49
- after(async () => {
50
- await knex.destroy()
51
- })
52
-
53
- beforeEach(async () => {
54
- await cleanTables(knex, ['test_user_settings'])
55
- })
56
-
57
- it('persists the provided resource id when posting simplified records to a custom-id resource', async () => {
58
- const created = await api.resources.user_settings.post({
59
- id: '7',
60
- theme: 'dark'
61
- })
62
-
63
- assert.equal(created.id, '7')
64
- assert.equal(created.theme, 'dark')
65
-
66
- const dbRow = await knex('test_user_settings').where('user_id', 7).first()
67
- assert.ok(dbRow)
68
- assert.equal(String(dbRow.user_id), '7')
69
- assert.equal(dbRow.theme, 'dark')
70
-
71
- const fetched = await api.resources.user_settings.get({ id: '7' })
72
- assert.equal(fetched.id, '7')
73
- assert.equal(fetched.theme, 'dark')
74
- })
75
- })