json-rest-api 1.0.29 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. package/LICENSE +10 -670
  2. package/LICENSE-GPL-3.0 +674 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +39 -30
  5. package/docs/API.md +744 -1273
  6. package/docs/GUIDE/01-getting-started.md +338 -0
  7. package/docs/GUIDE/02-resources-and-relationships.md +33 -0
  8. package/docs/GUIDE/03-running-example.md +87 -0
  9. package/docs/GUIDE/04-creating-and-querying.md +181 -0
  10. package/docs/GUIDE/05-belongs-to.md +177 -0
  11. package/docs/GUIDE/06-has-many.md +184 -0
  12. package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
  13. package/docs/GUIDE/08-many-to-many.md +184 -0
  14. package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
  15. package/docs/GUIDE/10-put-and-patch.md +174 -0
  16. package/docs/GUIDE/11-relationship-endpoints.md +174 -0
  17. package/docs/GUIDE/12-field-transformations.md +287 -0
  18. package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
  19. package/docs/GUIDE/14-custom-resource-methods.md +125 -0
  20. package/docs/GUIDE/15-query-projections.md +266 -0
  21. package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
  22. package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
  23. package/docs/GUIDE/18-labels.md +86 -0
  24. package/docs/GUIDE/19-managed-transactions.md +179 -0
  25. package/docs/GUIDE/20-transaction-outcomes.md +207 -0
  26. package/docs/GUIDE/21-schema-and-migrations.md +634 -0
  27. package/docs/GUIDE/22-version-field-migrations.md +208 -0
  28. package/docs/GUIDE/23-fastify.md +179 -0
  29. package/docs/GUIDE/24-cors.md +172 -0
  30. package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
  31. package/docs/GUIDE/26-file-uploads.md +278 -0
  32. package/docs/GUIDE/27-bulk-operations.md +277 -0
  33. package/docs/GUIDE/28-socketio.md +325 -0
  34. package/docs/GUIDE/29-writing-plugins.md +278 -0
  35. package/docs/GUIDE/30-backend-capabilities.md +100 -0
  36. package/docs/GUIDE/31-positioning.md +181 -0
  37. package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
  38. package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
  39. package/docs/GUIDE/index.md +51 -59
  40. package/docs/QUICKSTART.md +120 -287
  41. package/docs/architecture.md +75 -0
  42. package/docs/contributing.md +178 -0
  43. package/index.d.ts +36 -0
  44. package/index.js +1 -22
  45. package/lib/enhanced-logger.js +91 -85
  46. package/lib/error-context.js +445 -5
  47. package/lib/error-formatter.js +219 -59
  48. package/lib/knex-transaction.js +64 -0
  49. package/lib/logger-types.d.ts +22 -0
  50. package/lib/missing-package.js +4 -0
  51. package/lib/rest-api-errors.js +103 -8
  52. package/lib/runtime/json-rest-api.js +172 -0
  53. package/lib/transaction-types.d.ts +59 -0
  54. package/package.json +97 -27
  55. package/plugins/core/bulk-operations-plugin.js +191 -184
  56. package/plugins/core/connectors/express-plugin.js +148 -219
  57. package/plugins/core/connectors/fastify-plugin.js +125 -82
  58. package/plugins/core/connectors/lib/busboy-detector.js +74 -156
  59. package/plugins/core/connectors/lib/connector-core.js +144 -20
  60. package/plugins/core/connectors/lib/formidable-detector.js +63 -120
  61. package/plugins/core/connectors/lib/http-validators.js +77 -0
  62. package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
  63. package/plugins/core/connectors/lib/request-helpers.js +10 -1
  64. package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
  65. package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
  66. package/plugins/core/file-handling-plugin.js +87 -177
  67. package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
  68. package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
  69. package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
  70. package/plugins/core/lib/anyapi/schema-utils.js +25 -16
  71. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  72. package/plugins/core/lib/dbIntrospection.js +228 -21
  73. package/plugins/core/lib/dbTablesOperations.js +409 -92
  74. package/plugins/core/lib/fractional-positioning.js +12 -48
  75. package/plugins/core/lib/querying/identifier-query.js +19 -0
  76. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  77. package/plugins/core/lib/querying/include-to-many.js +428 -0
  78. package/plugins/core/lib/querying/include-to-one.js +579 -0
  79. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  80. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  81. package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
  82. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  83. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  84. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  85. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  86. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  87. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  88. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  89. package/plugins/core/lib/querying/query-constraint.js +27 -0
  90. package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
  91. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  92. package/plugins/core/lib/querying/sort-helpers.js +13 -1
  93. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  94. package/plugins/core/lib/querying/url-helpers.js +11 -4
  95. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  96. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  97. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  98. package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
  99. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  100. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  101. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  102. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  103. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  104. package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
  105. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  106. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  107. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  108. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  109. package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
  110. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  111. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  112. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  113. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  114. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  115. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  116. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  117. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  118. package/plugins/core/lib/storage/storage-adapter.js +128 -62
  119. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  120. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  121. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  122. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  123. package/plugins/core/lib/writing/resource-version.js +172 -0
  124. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  125. package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2485
  126. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  127. package/plugins/core/rest-api-cors-plugin.js +52 -205
  128. package/plugins/core/rest-api-knex-plugin.js +45 -1077
  129. package/plugins/core/rest-api-label-plugin.js +25 -38
  130. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  131. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  132. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  133. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  134. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  135. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  136. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  137. package/plugins/core/rest-api-plugin-methods/common.js +291 -395
  138. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  139. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  140. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
  141. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  142. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  143. package/plugins/core/rest-api-plugin-methods/get.js +36 -68
  144. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  145. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  146. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  147. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  148. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  149. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  150. package/plugins/core/rest-api-plugin-methods/query.js +37 -81
  151. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  152. package/plugins/core/rest-api-plugin.js +32 -46
  153. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  154. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  155. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  156. package/plugins/core/socketio-plugin.js +364 -344
  157. package/plugins/storage/local-storage.js +40 -36
  158. package/types/autofilter.d.ts +57 -0
  159. package/types/bulk-methods.d.ts +63 -0
  160. package/types/errors.d.ts +118 -0
  161. package/types/fastify-options.d.ts +19 -0
  162. package/types/file-detectors.d.ts +33 -0
  163. package/types/file-storage.d.ts +48 -0
  164. package/types/hook-context.d.ts +180 -0
  165. package/types/plugin-options.d.ts +53 -0
  166. package/types/relationship-methods.d.ts +74 -0
  167. package/types/representations.d.ts +50 -0
  168. package/types/resource-methods.d.ts +95 -0
  169. package/types/resource-schema.d.ts +157 -0
  170. package/types/row-policy.d.ts +31 -0
  171. package/types/runtime.d.ts +132 -0
  172. package/types/transactions.d.ts +21 -0
  173. package/.claude/agents/library-guide-expert.md +0 -30
  174. package/.claude/agents/rest-api-internals-expert.md +0 -54
  175. package/.claude/settings.local.json +0 -70
  176. package/AGENTS.md +0 -34
  177. package/CLAUDE.md +0 -160
  178. package/CRUSH.md +0 -55
  179. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  180. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  181. package/TODO.todo +0 -13
  182. package/connectors_ideas/fastify.js +0 -217
  183. package/connectors_ideas/koa-plugin.js +0 -112
  184. package/docs/.bundle/config +0 -2
  185. package/docs/COMPARISON.md +0 -433
  186. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  187. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  188. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  189. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  190. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  191. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  192. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  193. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
  194. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  195. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  196. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  197. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  198. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  199. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  200. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  201. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  202. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  203. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  204. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  205. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  206. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  207. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  208. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  209. package/docs/Gemfile +0 -6
  210. package/docs/Gemfile.lock +0 -308
  211. package/docs/ONBOARDING.md +0 -315
  212. package/docs/_config.yml +0 -36
  213. package/docs/_layouts/default.html +0 -123
  214. package/docs/apple-touch-icon.svg +0 -4
  215. package/docs/assets/css/style.scss +0 -392
  216. package/docs/favicon.svg +0 -4
  217. package/docs/index.md +0 -164
  218. package/docs/mini-guides/file-uploads-complete.md +0 -161
  219. package/eslint.config.js +0 -13
  220. package/examples/url-override-example.js +0 -137
  221. package/fixing_plan.md +0 -112
  222. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  223. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  224. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  225. package/plugins/storage/s3-storage.js +0 -93
  226. package/quickTest.js +0 -136
  227. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  228. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  229. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  230. package/scripts/run-docs.js +0 -133
  231. package/test-debug.js +0 -59
  232. package/tests/README.md +0 -202
  233. package/tests/TEST_TEMPLATE.test.js +0 -178
  234. package/tests/anyapi-basic.test.js +0 -105
  235. package/tests/anyapi-cursor-pagination.test.js +0 -148
  236. package/tests/anyapi-custom-idproperty.test.js +0 -163
  237. package/tests/autofilter.test.js +0 -463
  238. package/tests/bulk-operations.test.js +0 -657
  239. package/tests/computed-fields.test.js +0 -586
  240. package/tests/cors.test.js +0 -834
  241. package/tests/curl.test.js +0 -467
  242. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  243. package/tests/custom-id-simplified-post.test.js +0 -75
  244. package/tests/custom-idproperty-relationships.test.js +0 -932
  245. package/tests/db-introspection.test.js +0 -521
  246. package/tests/db-table-operations.test.js +0 -798
  247. package/tests/fastify-plugin.test.js +0 -184
  248. package/tests/field-getters.test.js +0 -370
  249. package/tests/field-setters.test.js +0 -306
  250. package/tests/file-handling.test.js +0 -155
  251. package/tests/fixtures/api-configs.js +0 -2566
  252. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  253. package/tests/helpers/fake-fastify.js +0 -99
  254. package/tests/helpers/storage-mode.js +0 -65
  255. package/tests/helpers/test-utils.js +0 -203
  256. package/tests/http-connectors-parity.test.js +0 -395
  257. package/tests/id-normalization.test.js +0 -444
  258. package/tests/include-depth-validation.test.js +0 -331
  259. package/tests/includes.test.js +0 -427
  260. package/tests/local-storage.test.js +0 -96
  261. package/tests/logical-id-write-validation.test.js +0 -151
  262. package/tests/nested-includes.test.js +0 -617
  263. package/tests/pagination-boundaries.test.js +0 -154
  264. package/tests/pagination-contract.test.js +0 -122
  265. package/tests/pagination-cursor-multifield.test.js +0 -641
  266. package/tests/pagination-enhanced.test.js +0 -563
  267. package/tests/polymorphic-relationships.test.js +0 -347
  268. package/tests/polymorphic-simple.test.js +0 -148
  269. package/tests/positioning-advanced.test.js +0 -692
  270. package/tests/positioning-extended.test.js +0 -915
  271. package/tests/positioning-multifilter.test.js +0 -699
  272. package/tests/positioning.test.js +0 -560
  273. package/tests/post-commit-hooks.test.js +0 -120
  274. package/tests/projected-fields.test.js +0 -319
  275. package/tests/public-exports.test.js +0 -15
  276. package/tests/put-create-parity.test.js +0 -216
  277. package/tests/queries.test.js +0 -521
  278. package/tests/query-limits.test.js +0 -584
  279. package/tests/relationship-endpoints.test.js +0 -620
  280. package/tests/relationship-include-storage-adapter.test.js +0 -56
  281. package/tests/relationships.test.js +0 -681
  282. package/tests/rest-api.test.js +0 -337
  283. package/tests/return-record-settings.test.js +0 -184
  284. package/tests/row-policy.test.js +0 -489
  285. package/tests/s3-storage.test.js +0 -38
  286. package/tests/searchschema-knex-behavior.test.js +0 -187
  287. package/tests/searchschema-merge.test.js +0 -151
  288. package/tests/socketio.test.js +0 -860
  289. package/tests/storage-mapping.test.js +0 -362
  290. package/tests/temporal-boundaries.test.js +0 -208
  291. package/tests/test-development-guide.md +0 -370
  292. package/tests/virtual-fields.test.js +0 -404
@@ -1,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
- })