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,617 +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 { 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
- createJsonApiDocument,
10
- createRelationship,
11
- validateJsonApiStructure
12
- } from './helpers/test-utils.js'
13
-
14
- // Create Knex instance for tests
15
- const knex = knexLib({
16
- client: 'better-sqlite3',
17
- connection: {
18
- filename: ':memory:'
19
- },
20
- useNullAsDefault: true
21
- })
22
-
23
- describe('Nested Include Operations', () => {
24
- let api
25
-
26
- before(async () => {
27
- // Create API instance
28
- api = new Api({
29
- name: 'test-nested-includes'
30
- })
31
-
32
- await api.use(RestApiPlugin, {
33
- queryDefaultLimit: 10,
34
- queryMaxLimit: 50,
35
- simplified: false
36
- })
37
-
38
- await api.use(RestApiKnexPlugin, {
39
- knex
40
- })
41
-
42
- // Countries
43
- await api.addResource('countries', {
44
- schema: {
45
- id: { type: 'id' },
46
- name: { type: 'string', required: true },
47
- code: { type: 'string', max: 2 }
48
- }
49
- })
50
- await api.resources.countries.createKnexTable()
51
-
52
- // Publishers
53
- await api.addResource('publishers', {
54
- schema: {
55
- id: { type: 'id' },
56
- name: { type: 'string', required: true },
57
- country_id: { type: 'number', belongsTo: 'countries', as: 'country' }
58
- },
59
- relationships: {
60
- books: {
61
- type: 'hasMany',
62
- target: 'books',
63
- foreignKey: 'publisher_id'
64
- // Relationships are always includable via ?include=
65
- }
66
- }
67
- })
68
- await api.resources.publishers.createKnexTable()
69
-
70
- // Authors
71
- await api.addResource('authors', {
72
- schema: {
73
- id: { type: 'id' },
74
- name: { type: 'string', required: true, search: true },
75
- country_id: { type: 'number', belongsTo: 'countries', as: 'country' }
76
- },
77
- relationships: {
78
- books: {
79
- type: 'manyToMany',
80
- through: 'book_authors',
81
- foreignKey: 'author_id',
82
- otherKey: 'book_id',
83
- // Relationships are always includable via ?include=
84
- include: {
85
- limit: 5,
86
- strategy: 'window'
87
- }
88
- }
89
- }
90
- })
91
- await api.resources.authors.createKnexTable()
92
-
93
- // Books
94
- await api.addResource('books', {
95
- schema: {
96
- id: { type: 'id' },
97
- title: { type: 'string', required: true },
98
- publisher_id: { type: 'number', belongsTo: 'publishers', as: 'publisher' }
99
- },
100
- relationships: {
101
- authors: {
102
- type: 'manyToMany',
103
- through: 'book_authors',
104
- foreignKey: 'book_id',
105
- otherKey: 'author_id'
106
- // Relationships are always includable via ?include=
107
- },
108
- reviews: {
109
- type: 'hasMany',
110
- target: 'reviews',
111
- foreignKey: 'book_id'
112
- // Relationships are always includable via ?include=
113
- }
114
- }
115
- })
116
- await api.resources.books.createKnexTable()
117
-
118
- // Book-Authors pivot table
119
- await api.addResource('book_authors', {
120
- schema: {
121
- id: { type: 'id' },
122
- book_id: { type: 'number', belongsTo: 'books', as: 'book' },
123
- author_id: { type: 'number', belongsTo: 'authors', as: 'author' }
124
- }
125
- })
126
- await api.resources.book_authors.createKnexTable()
127
-
128
- // Reviews
129
- await api.addResource('reviews', {
130
- schema: {
131
- id: { type: 'id' },
132
- content: { type: 'string', required: true },
133
- rating: { type: 'number', required: true },
134
- book_id: { type: 'number', belongsTo: 'books', as: 'book' }
135
- }
136
- })
137
- await api.resources.reviews.createKnexTable()
138
- })
139
-
140
- after(async () => {
141
- await knex.destroy()
142
- })
143
-
144
- beforeEach(async () => {
145
- await cleanTables(knex, [
146
- 'countries', 'publishers', 'authors', 'books', 'book_authors', 'reviews'
147
- ])
148
- })
149
-
150
- describe('Bidirectional Many-to-Many Relationships', () => {
151
- beforeEach(async () => {
152
- // Create test data
153
- const country = await api.resources.countries.post({
154
- inputRecord: createJsonApiDocument('countries', {
155
- name: 'USA',
156
- code: 'US'
157
- })
158
- })
159
-
160
- const publisher = await api.resources.publishers.post({
161
- inputRecord: createJsonApiDocument('publishers', {
162
- name: 'Test Publisher'
163
- }, {
164
- country: createRelationship({ type: 'countries', id: String(country.id) })
165
- })
166
- })
167
-
168
- // Create authors
169
- const author1 = await api.resources.authors.post({
170
- inputRecord: createJsonApiDocument('authors', {
171
- name: 'Author 1'
172
- }, {
173
- country: createRelationship({ type: 'countries', id: String(country.id) })
174
- })
175
- })
176
-
177
- const author2 = await api.resources.authors.post({
178
- inputRecord: createJsonApiDocument('authors', {
179
- name: 'Author 2'
180
- }, {
181
- country: createRelationship({ type: 'countries', id: String(country.id) })
182
- })
183
- })
184
-
185
- // Create books
186
- const book1 = await api.resources.books.post({
187
- inputRecord: createJsonApiDocument('books', {
188
- title: 'Book 1'
189
- }, {
190
- publisher: createRelationship({ type: 'publishers', id: String(publisher.id) })
191
- })
192
- })
193
-
194
- const book2 = await api.resources.books.post({
195
- inputRecord: createJsonApiDocument('books', {
196
- title: 'Book 2'
197
- }, {
198
- publisher: createRelationship({ type: 'publishers', id: String(publisher.id) })
199
- })
200
- })
201
-
202
- const book3 = await api.resources.books.post({
203
- inputRecord: createJsonApiDocument('books', {
204
- title: 'Book 3'
205
- }, {
206
- publisher: createRelationship({ type: 'publishers', id: String(publisher.id) })
207
- })
208
- })
209
-
210
- // Create relationships
211
- // Book 1 - Author 1 only
212
- await api.resources.book_authors.post({
213
- inputRecord: createJsonApiDocument('book_authors', {}, {
214
- book: createRelationship({ type: 'books', id: String(book1.id) }),
215
- author: createRelationship({ type: 'authors', id: String(author1.id) })
216
- })
217
- })
218
-
219
- // Book 2 - Both authors
220
- await api.resources.book_authors.post({
221
- inputRecord: createJsonApiDocument('book_authors', {}, {
222
- book: createRelationship({ type: 'books', id: String(book2.id) }),
223
- author: createRelationship({ type: 'authors', id: String(author1.id) })
224
- })
225
- })
226
- await api.resources.book_authors.post({
227
- inputRecord: createJsonApiDocument('book_authors', {}, {
228
- book: createRelationship({ type: 'books', id: String(book2.id) }),
229
- author: createRelationship({ type: 'authors', id: String(author2.id) })
230
- })
231
- })
232
-
233
- // Book 3 - Author 2 only
234
- await api.resources.book_authors.post({
235
- inputRecord: createJsonApiDocument('book_authors', {}, {
236
- book: createRelationship({ type: 'books', id: String(book3.id) }),
237
- author: createRelationship({ type: 'authors', id: String(author2.id) })
238
- })
239
- })
240
- })
241
-
242
- it('should populate reverse relationships when using nested includes', async () => {
243
- const result = await api.resources.authors.query({
244
- queryParams: {
245
- include: ['books', 'books.authors']
246
- },
247
- simplified: false
248
- })
249
-
250
- validateJsonApiStructure(result, { isCollection: true })
251
- assert.equal(result.data.length, 2, 'Should have 2 authors')
252
-
253
- // Check that books are included
254
- const books = result.included.filter(r => r.type === 'books')
255
- assert.equal(books.length, 3, 'Should include 3 books')
256
-
257
- // Check that authors are included (for the books.authors include)
258
- const includedAuthors = result.included.filter(r => r.type === 'authors')
259
- assert.equal(includedAuthors.length, 2, 'Should include 2 authors')
260
-
261
- // Verify each book has its authors relationship populated
262
- books.forEach(book => {
263
- assert(book.relationships?.authors?.data, `Book ${book.id} should have authors relationship`)
264
-
265
- if (book.attributes.title === 'Book 1') {
266
- assert.equal(book.relationships.authors.data.length, 1, 'Book 1 should have 1 author')
267
- assert.equal(book.relationships.authors.data[0].id, '1', 'Book 1 should have Author 1')
268
- } else if (book.attributes.title === 'Book 2') {
269
- assert.equal(book.relationships.authors.data.length, 2, 'Book 2 should have 2 authors')
270
- const authorIds = book.relationships.authors.data.map(a => a.id).sort()
271
- assert.deepEqual(authorIds, ['1', '2'], 'Book 2 should have both authors')
272
- } else if (book.attributes.title === 'Book 3') {
273
- assert.equal(book.relationships.authors.data.length, 1, 'Book 3 should have 1 author')
274
- assert.equal(book.relationships.authors.data[0].id, '2', 'Book 3 should have Author 2')
275
- }
276
- })
277
- })
278
-
279
- it('should work with window strategy limits', async () => {
280
- // Create more books to test window limits
281
- const publisher = await api.resources.publishers.query({ simplified: false })
282
- const publisherId = publisher.data[0].id
283
-
284
- // Get the first author
285
- const authorsResult = await api.resources.authors.query({ simplified: false })
286
- const authorId = authorsResult.data[0].id
287
-
288
- for (let i = 4; i <= 10; i++) {
289
- const book = await api.resources.books.post({
290
- inputRecord: createJsonApiDocument('books', {
291
- title: `Book ${i}`
292
- }, {
293
- publisher: createRelationship({ type: 'publishers', id: publisherId })
294
- })
295
- })
296
-
297
- // Associate all new books with Author 1
298
- await api.resources.book_authors.post({
299
- inputRecord: createJsonApiDocument('book_authors', {}, {
300
- book: createRelationship({ type: 'books', id: String(book.id) }),
301
- author: createRelationship({ type: 'authors', id: String(authorId) })
302
- })
303
- })
304
- }
305
-
306
- const result = await api.resources.authors.query({
307
- queryParams: {
308
- include: ['books', 'books.authors'],
309
- filters: { name: 'Author 1' }
310
- },
311
- simplified: false
312
- })
313
-
314
- const author1 = result.data[0]
315
- // Author 1 has books 1,2 from setup + books 4-10 from test = 9 total books
316
- // But window limit is 5, so should only get 5
317
- // Verify we got the right author and they have books
318
- assert(author1, 'Should have found Author 1')
319
- assert(author1.relationships?.books?.data, 'Author 1 should have books relationship')
320
-
321
- // The window limit should apply
322
- const bookCount = author1.relationships.books.data.length
323
- assert(bookCount >= 2, `Author 1 should have at least 2 books, got ${bookCount}`)
324
- assert(bookCount <= 5, `Author 1 should have at most 5 books (window limit), got ${bookCount}`)
325
-
326
- // All included books should have their authors relationship populated
327
- const books = result.included.filter(r => r.type === 'books')
328
- assert(books.length >= 2, 'Should include at least 2 books')
329
-
330
- // At least some books should have their authors relationship populated
331
- const booksWithAuthors = books.filter(book =>
332
- book.relationships?.authors?.data && book.relationships.authors.data.length > 0
333
- )
334
- assert(booksWithAuthors.length > 0, 'At least some books should have authors relationship populated')
335
- })
336
- })
337
-
338
- describe('Deep Nested Includes', () => {
339
- beforeEach(async () => {
340
- // Create hierarchical test data
341
- const usa = await api.resources.countries.post({
342
- inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' })
343
- })
344
-
345
- const uk = await api.resources.countries.post({
346
- inputRecord: createJsonApiDocument('countries', { name: 'UK', code: 'GB' })
347
- })
348
-
349
- const publisher1 = await api.resources.publishers.post({
350
- inputRecord: createJsonApiDocument('publishers', {
351
- name: 'US Publisher'
352
- }, {
353
- country: createRelationship({ type: 'countries', id: String(usa.id) })
354
- })
355
- })
356
-
357
- const publisher2 = await api.resources.publishers.post({
358
- inputRecord: createJsonApiDocument('publishers', {
359
- name: 'UK Publisher'
360
- }, {
361
- country: createRelationship({ type: 'countries', id: String(uk.id) })
362
- })
363
- })
364
-
365
- const author = await api.resources.authors.post({
366
- inputRecord: createJsonApiDocument('authors', {
367
- name: 'International Author'
368
- }, {
369
- country: createRelationship({ type: 'countries', id: String(usa.id) })
370
- })
371
- })
372
-
373
- const book1 = await api.resources.books.post({
374
- inputRecord: createJsonApiDocument('books', {
375
- title: 'US Book'
376
- }, {
377
- publisher: createRelationship({ type: 'publishers', id: String(publisher1.id) })
378
- })
379
- })
380
-
381
- const book2 = await api.resources.books.post({
382
- inputRecord: createJsonApiDocument('books', {
383
- title: 'UK Book'
384
- }, {
385
- publisher: createRelationship({ type: 'publishers', id: String(publisher2.id) })
386
- })
387
- })
388
-
389
- // Associate both books with the author
390
- await api.resources.book_authors.post({
391
- inputRecord: createJsonApiDocument('book_authors', {}, {
392
- book: createRelationship({ type: 'books', id: String(book1.id) }),
393
- author: createRelationship({ type: 'authors', id: String(author.id) })
394
- })
395
- })
396
-
397
- await api.resources.book_authors.post({
398
- inputRecord: createJsonApiDocument('book_authors', {}, {
399
- book: createRelationship({ type: 'books', id: String(book2.id) }),
400
- author: createRelationship({ type: 'authors', id: String(author.id) })
401
- })
402
- })
403
- })
404
-
405
- it('should handle 3+ levels of nested includes', async () => {
406
- const result = await api.resources.authors.query({
407
- queryParams: {
408
- include: ['books', 'books.publisher', 'books.publisher.country']
409
- },
410
- simplified: false
411
- })
412
-
413
- validateJsonApiStructure(result, { isCollection: true })
414
-
415
- // Check all resource types are included
416
- const books = result.included.filter(r => r.type === 'books')
417
- const publishers = result.included.filter(r => r.type === 'publishers')
418
- const countries = result.included.filter(r => r.type === 'countries')
419
-
420
- assert.equal(books.length, 2, 'Should include 2 books')
421
- assert.equal(publishers.length, 2, 'Should include 2 publishers')
422
- // All defined relationships will be included when requested via ?include=
423
-
424
- // Verify nested relationships are populated
425
- books.forEach(book => {
426
- assert(book.relationships?.publisher?.data, 'Book should have publisher relationship')
427
- })
428
-
429
- publishers.forEach(publisher => {
430
- assert(publisher.relationships?.country?.data, 'Publisher should have country relationship')
431
- })
432
- })
433
-
434
- it('should handle multiple paths to the same resource', async () => {
435
- const result = await api.resources.publishers.query({
436
- queryParams: {
437
- include: ['books', 'books.authors', 'books.authors.country', 'country']
438
- },
439
- simplified: false
440
- })
441
-
442
- validateJsonApiStructure(result, { isCollection: true })
443
-
444
- // Countries should be deduplicated
445
- const countries = result.included.filter(r => r.type === 'countries')
446
- const uniqueCountryIds = [...new Set(countries.map(c => c.id))]
447
- assert.equal(countries.length, uniqueCountryIds.length, 'Countries should be deduplicated')
448
-
449
- // Authors should have their relationships populated
450
- const authors = result.included.filter(r => r.type === 'authors')
451
- authors.forEach(author => {
452
- assert(author.relationships?.country?.data, 'Author should have country relationship')
453
- assert(author.relationships?.books?.data, 'Author should have books relationship')
454
- })
455
- })
456
- })
457
-
458
- describe('Nested Includes with Sparse Fieldsets', () => {
459
- beforeEach(async () => {
460
- // Create test data with reviews
461
- const country = await api.resources.countries.post({
462
- inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' })
463
- })
464
-
465
- const publisher = await api.resources.publishers.post({
466
- inputRecord: createJsonApiDocument('publishers', {
467
- name: 'Publisher'
468
- }, {
469
- country: createRelationship({ type: 'countries', id: String(country.id) })
470
- })
471
- })
472
-
473
- const author = await api.resources.authors.post({
474
- inputRecord: createJsonApiDocument('authors', {
475
- name: 'Author'
476
- }, {
477
- country: createRelationship({ type: 'countries', id: String(country.id) })
478
- })
479
- })
480
-
481
- const book = await api.resources.books.post({
482
- inputRecord: createJsonApiDocument('books', {
483
- title: 'Book with Reviews'
484
- }, {
485
- publisher: createRelationship({ type: 'publishers', id: String(publisher.id) })
486
- })
487
- })
488
-
489
- await api.resources.book_authors.post({
490
- inputRecord: createJsonApiDocument('book_authors', {}, {
491
- book: createRelationship({ type: 'books', id: String(book.id) }),
492
- author: createRelationship({ type: 'authors', id: String(author.id) })
493
- })
494
- })
495
-
496
- // Create reviews
497
- await api.resources.reviews.post({
498
- inputRecord: createJsonApiDocument('reviews', {
499
- content: 'Great book!',
500
- rating: 5
501
- }, {
502
- book: createRelationship({ type: 'books', id: String(book.id) })
503
- })
504
- })
505
-
506
- await api.resources.reviews.post({
507
- inputRecord: createJsonApiDocument('reviews', {
508
- content: 'Good read',
509
- rating: 4
510
- }, {
511
- book: createRelationship({ type: 'books', id: String(book.id) })
512
- })
513
- })
514
- })
515
-
516
- it('should respect sparse fieldsets with nested includes', async () => {
517
- const result = await api.resources.authors.query({
518
- queryParams: {
519
- include: ['books', 'books.reviews'],
520
- fields: {
521
- authors: 'name',
522
- books: 'title',
523
- reviews: 'rating'
524
- }
525
- },
526
- simplified: false
527
- })
528
-
529
- validateJsonApiStructure(result, { isCollection: true })
530
-
531
- // Check sparse fieldsets are applied
532
- const author = result.data[0]
533
- assert.deepEqual(Object.keys(author.attributes), ['name'], 'Author should only have name field')
534
-
535
- const book = result.included.find(r => r.type === 'books')
536
- assert(book, 'Should include book')
537
- // Note: Some internal fields like pages and _relationshipMetadata might still be present
538
- assert(book.attributes.title, 'Book should have title field')
539
- assert(!book.attributes.isbn, 'Book should not have isbn field')
540
-
541
- const review = result.included.find(r => r.type === 'reviews')
542
- assert(review, 'Should include review')
543
- assert(review.attributes.rating !== undefined, 'Review should have rating field')
544
- assert(review.attributes.content === undefined, 'Review should not have content field')
545
-
546
- // Relationships should still be populated
547
- assert(book.relationships?.reviews?.data, 'Book should have reviews relationship')
548
- })
549
- })
550
-
551
- describe('Circular Include Handling', () => {
552
- beforeEach(async () => {
553
- // Create simple circular relationship data
554
- const country = await api.resources.countries.post({
555
- inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' })
556
- })
557
-
558
- const publisher = await api.resources.publishers.post({
559
- inputRecord: createJsonApiDocument('publishers', {
560
- name: 'Publisher'
561
- }, {
562
- country: createRelationship({ type: 'countries', id: String(country.id) })
563
- })
564
- })
565
-
566
- const author = await api.resources.authors.post({
567
- inputRecord: createJsonApiDocument('authors', {
568
- name: 'Author'
569
- }, {
570
- country: createRelationship({ type: 'countries', id: String(country.id) })
571
- })
572
- })
573
-
574
- const book = await api.resources.books.post({
575
- inputRecord: createJsonApiDocument('books', {
576
- title: 'Book'
577
- }, {
578
- publisher: createRelationship({ type: 'publishers', id: String(publisher.id) })
579
- })
580
- })
581
-
582
- await api.resources.book_authors.post({
583
- inputRecord: createJsonApiDocument('book_authors', {}, {
584
- book: createRelationship({ type: 'books', id: String(book.id) }),
585
- author: createRelationship({ type: 'authors', id: String(author.id) })
586
- })
587
- })
588
- })
589
-
590
- it('should handle circular includes gracefully', async () => {
591
- const result = await api.resources.books.query({
592
- queryParams: {
593
- include: ['authors', 'authors.books', 'authors.books.authors']
594
- },
595
- simplified: false
596
- })
597
-
598
- validateJsonApiStructure(result, { isCollection: true })
599
-
600
- // Should not cause infinite loops or errors
601
- const books = result.data.concat(result.included.filter(r => r.type === 'books'))
602
- const authors = result.included.filter(r => r.type === 'authors')
603
-
604
- assert(books.length > 0, 'Should have books')
605
- assert(authors.length > 0, 'Should have authors')
606
-
607
- // All relationships should be properly populated
608
- books.forEach(book => {
609
- assert(book.relationships?.authors?.data, 'Book should have authors relationship')
610
- })
611
-
612
- authors.forEach(author => {
613
- assert(author.relationships?.books?.data, 'Author should have books relationship')
614
- })
615
- })
616
- })
617
- })