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,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
- })