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,898 +0,0 @@
1
- # 2.5 hasMany records (polymorphic)
2
-
3
- Polymorphic relationships are a special type of one-to-many association where a single resource can belong to *one of several different* resource types. For example, a `review` might be associated with an `author` *or* a `publisher*. This differs from a standard `belongsTo` where a resource belongs to only one specific type.
4
-
5
- To implement this, the "belonging" resource (e.g., `review`) needs two foreign key fields:
6
- 1. An **`idField`** (e.g., `reviewable_id`) to store the ID of the related resource.
7
- 2. A **`typeField`** (e.g., `reviewable_type`) to store the *type* of the related resource (e.g., 'authors' or 'publishers').
8
-
9
- `json-rest-api` supports polymorphic relationships via the **`belongsToPolymorphic`** definition on the *child* resource. To establish the reverse `hasMany` link from the parent, you simply use the **`via`** keyword, pointing to the name of the `belongsToPolymorphic` field on the child.
10
-
11
- For this section, we'll use our existing `publishers` and `authors` tables, and introduce a new `reviews` table that can give reviews to both authors and publishers.
12
-
13
- ```javascript
14
- // Define publishers resource
15
- await api.addResource('publishers', {
16
- schema: {
17
- name: { type: 'string', required: true, max: 255, search: true, indexed: true},
18
- },
19
- relationships: {
20
- authors: { type: 'hasMany', target: 'authors', foreignKey: 'publisher_id' },
21
- reviews: { type: 'hasMany', target: 'reviews', via: 'reviewable' } // Polymorphic relationship
22
- },
23
- });
24
- await api.resources.publishers.createKnexTable();
25
-
26
- // Define authors resource
27
- await api.addResource('authors', {
28
- schema: {
29
- name: { type: 'string', required: true, max: 100, search: true },
30
- surname: { type: 'string', required: true, max: 100, search: true },
31
- publisher_id: { type: 'id', belongsTo: 'publishers', as: 'publisher', nullable: true }
32
- },
33
- relationships: {
34
- reviews: { type: 'hasMany', target: 'reviews', via: 'reviewable' } // Polymorphic relationship
35
- },
36
- });
37
- await api.resources.authors.createKnexTable();
38
-
39
- // Define reviews resource with a polymorphic relationship
40
- await api.addResource('reviews', {
41
- schema: {
42
- rating: { type: 'number', required: true, min: 1, max: 5 },
43
- comment: { type: 'string', max: 500, nullable: true },
44
- // These two fields store the polymorphic relationship data in the database
45
- reviewable_type: { type: 'string', max: 50, required: true },
46
- reviewable_id: { type: 'id', required: true },
47
- },
48
- relationships: {
49
- reviewable: {
50
- belongsToPolymorphic: {
51
- types: ['publishers', 'authors'], // The possible resource types this review can belong to
52
- typeField: 'reviewable_type', // The field in 'reviews' schema storing the parent's type
53
- idField: 'reviewable_id' // The field in 'reviews' schema storing the parent's ID
54
- }
55
- }
56
- },
57
- });
58
- await api.resources.reviews.createKnexTable();
59
- ```
60
-
61
- Defining a polymorphic relationship is more verbose because there are two fields to take care of insteaf of one. So, the parent record can't just say "all records in the child relationship where the field `foreign_id` matches my id" -- it would need to specify two fields, not one: the foreign ID and the table the ID refers to.
62
-
63
- This is why the child table defines a `reviewable` object in `relationships` where it states which fields are used for the relationship:
64
-
65
- ```javascript
66
- // Child table, e.g. 'reviews' (applicable to publishers and authors)
67
- relationships: {
68
- reviewable: {
69
- belongsToPolymorphic: {
70
- types: ['publishers', 'authors'],
71
- typeField: 'reviewable_type',
72
- idField: 'reviewable_id'
73
- }
74
- }
75
- },
76
- ```
77
- So when the parent defines the relationship, they just have to mention `reviewable`:
78
-
79
- ```javascript
80
- relationships: {
81
- reviews: { type: 'hasMany', target: 'reviews', via: 'reviewable' } // Polymorphic relationship
82
- },
83
- ```
84
-
85
- ## Field Visibility and Polymorphic Relationships
86
-
87
- Polymorphic relationships are a prime example of json-rest-api's database-first philosophy and field abstraction:
88
-
89
- **Database Reality (What Backend Developers Define):**
90
- - Schema includes `reviewable_type` and `reviewable_id` fields
91
- - These are actual database columns that store the polymorphic relationship
92
- - Hooks receive these field names in `context.belongsToUpdates`
93
-
94
- **API Abstraction (What Consumers See):**
95
- - Input: Use `reviewable` relationship object instead of type/id fields
96
- - Output: Returns `reviewable: { id: '123', _type: 'authors' }`
97
- - The `reviewable_type` and `reviewable_id` fields are hidden from API responses
98
-
99
- This ensures API consumers work with clean relationship objects while backend developers maintain full control over the database implementation.
100
-
101
- `json-rest-api` provides flexibility in how you provide data for polymorphic relationships when creating new records, depending on whether you're using the simplified API mode (default for programmatic calls) or the strict JSON:API format.
102
-
103
- Here is how to add reviews, both in simplified and non-simplified mode:
104
-
105
- ```javascript
106
- const frenchPublisher_ns = await api.resources.publishers.post({ name: 'French Books Inc. (NS)' });
107
- const germanPublisher_ns = await api.resources.publishers.post({ name: 'German Press GmbH (NS)' });
108
-
109
- const frenchAuthor1_ns = await api.resources.authors.post({ name: 'Victor (NS)', surname: 'Hugo (NS)', publisher: frenchPublisher_ns.id });
110
- const germanAuthor_ns = await api.resources.authors.post({ name: 'Johann (NS)', surname: 'Goethe (NS)', publisher: germanPublisher_ns.id });
111
-
112
- const review1_simplified = await api.resources.reviews.post({
113
- rating: 4,
114
- comment: 'Great German author! (Simplified)',
115
- reviewable: { id: germanAuthor_ns.id, _type: 'authors' }
116
- });
117
-
118
- const review2_simplified = await api.resources.reviews.post({
119
- rating: 1,
120
- comment: 'I do not enjoy their books',
121
- reviewable: { id: frenchPublisher_ns.id, _type: 'publishers' }
122
- });
123
-
124
-
125
- // Add reviews using non-simplified (JSON:API standard) input with relationships object
126
- const review3_non_simplified = await api.resources.reviews.post({
127
- inputRecord: {
128
- data: {
129
- type: 'reviews',
130
- attributes: {
131
- rating: 3,
132
- comment: 'Decent publisher, some good titles (NS).'
133
- },
134
- relationships: { // Explicitly define the polymorphic relationship here
135
- reviewable: {
136
- data: { type: 'publishers', id: frenchPublisher_ns.id } // Resource identifier object
137
- }
138
- }
139
- }
140
- },
141
- simplified: false // Ensure non-simplified mode for this call
142
- });
143
-
144
- const review4_non_simplified = await api.resources.reviews.post({
145
- inputRecord: {
146
- data: {
147
- type: 'reviews',
148
- attributes: {
149
- rating: 5,
150
- comment: 'Hugo is a master storyteller! (NS)'
151
- },
152
- relationships: {
153
- reviewable: {
154
- data: { type: 'authors', id: frenchAuthor1_ns.id }
155
- }
156
- }
157
- }
158
- },
159
- simplified: false
160
- });
161
-
162
- const frenchAuthor1_with_reviews_simplified = await api.resources.authors.get({ id: frenchAuthor1_ns.id });
163
- const frenchAuthor1_with_reviews_non_simplified = await api.resources.authors.get({ id: frenchAuthor1_ns.id, simplified: false });
164
-
165
- const french_authors_simplified = await api.resources.authors.query({});
166
- // HTTP: GET /api/authors
167
- // Returns (simplified): [
168
- // { id: '1', name: 'Victor Hugo', surname: 'Hugo', publisher: { id: '1' } },
169
- // { id: '2', name: 'Alexandre Dumas', surname: 'Dumas', publisher: { id: '1' } }
170
- // ]
171
-
172
- const french_authors_non_simplified = await api.resources.authors.query({simplified: false });
173
- // HTTP: GET /api/authors
174
- // Returns (JSON:API): {
175
- // data: [
176
- // { type: 'authors', id: '1', attributes: { name: 'Victor Hugo' } },
177
- // { type: 'authors', id: '2', attributes: { name: 'Alexandre Dumas' } }
178
- // ]
179
- // }
180
-
181
- console.log('Added Publisher Review (simplified):', inspect(review1_simplified));
182
- console.log('Added Author Review (simplified):', inspect(review2_simplified));
183
- console.log('Added Publisher Review (non-Simplified):', inspect(review3_non_simplified));
184
- console.log('Added Author Review (non-Simplified):', inspect(review4_non_simplified));
185
-
186
- // Single records
187
- console.log('French author with the newly added reviews (simplified):')
188
- console.log(inspect(frenchAuthor1_with_reviews_simplified));
189
- console.log('French author with the newly added reviews (non-simplified):')
190
- console.log(inspect(frenchAuthor1_with_reviews_non_simplified))
191
-
192
- // Lists
193
- console.log('French authors with the newly added reviews (simplified):')
194
- console.log(inspect(french_authors_simplified));
195
- console.log('French authors with the newly added reviews (non-simplified):')
196
- console.log(inspect(french_authors_non_simplified))
197
-
198
-
199
- ```
200
-
201
- As you can see, you can add reviews both in simplified and non-simplified mode. Here is the difference:
202
-
203
- * **Simplified Mode:** `json-rest-api` automatically recognizes `reviewable_type` and `reviewable_id` attributes as foreign keys for polymorphic relationships defined with `belongsToPolymorphic`. This provides a flattened, convenient syntax for programmatic use.
204
- * **Non-Simplified Mode (JSON:API Standard):** This adheres to the JSON:API specification, where relationships are explicitly defined in the `relationships` object with a resource identifier object (`{ type: 'resourceType', id: 'resourceId' }`). This is typically used when interacting with the API via HTTP or when strict JSON:API compliance is required.
205
-
206
- **Expected Output**
207
-
208
- ```text
209
- Added Publisher Review (simplified): { id: '1', rating: 4, comment: 'Great German author! (Simplified)' }
210
- Added Author Review (simplified): { id: '2', rating: 1, comment: 'I do not enjoy their books' }
211
- Added Publisher Review (non-Simplified): {
212
- data: {
213
- type: 'reviews',
214
- id: '3',
215
- attributes: { rating: 3, comment: 'Decent publisher, some good titles (NS).' },
216
- links: { self: '/api/reviews/3' }
217
- },
218
- links: { self: '/api/reviews/3' }
219
- }
220
- Added Author Review (non-Simplified): {
221
- data: {
222
- type: 'reviews',
223
- id: '4',
224
- attributes: { rating: 5, comment: 'Hugo is a master storyteller! (NS)' },
225
- links: { self: '/api/reviews/4' }
226
- },
227
- links: { self: '/api/reviews/4' }
228
- }
229
- French author with the newly added reviews (simplified):
230
- {
231
- id: '1',
232
- name: 'Victor (NS)',
233
- surname: 'Hugo (NS)',
234
- reviews: [ { id: '4' } ],
235
- publisher: { id: '1' }
236
- }
237
- French author with the newly added reviews (non-simplified):
238
- {
239
- data: {
240
- type: 'authors',
241
- id: '1',
242
- attributes: { name: 'Victor (NS)', surname: 'Hugo (NS)' },
243
- relationships: {
244
- reviews: { data: [ { type: 'reviews', id: '4' } ] },
245
- publisher: {
246
- data: { type: 'publishers', id: '1' },
247
- links: {
248
- self: '/api/authors/1/relationships/publisher',
249
- related: '/api/authors/1/publisher'
250
- }
251
- }
252
- },
253
- links: { self: '/api/authors/1' }
254
- },
255
- links: { self: '/api/authors/1' }
256
- }
257
- French authors with the newly added reviews (simplified):
258
- [
259
- {
260
- id: '1',
261
- name: 'Victor (NS)',
262
- surname: 'Hugo (NS)',
263
- reviews: [ { id: '4' } ],
264
- publisher: { id: '1' }
265
- },
266
- {
267
- id: '2',
268
- name: 'Johann (NS)',
269
- surname: 'Goethe (NS)',
270
- reviews: [ { id: '1' } ],
271
- publisher: { id: '2' }
272
- }
273
- ]
274
- French authors with the newly added reviews (non-simplified):
275
- {
276
- data: [
277
- {
278
- type: 'authors',
279
- id: '1',
280
- attributes: { name: 'Victor (NS)', surname: 'Hugo (NS)' },
281
- relationships: {
282
- reviews: { data: [ { type: 'reviews', id: '4' } ] },
283
- publisher: {
284
- data: { type: 'publishers', id: '1' },
285
- links: {
286
- self: '/api/authors/1/relationships/publisher',
287
- related: '/api/authors/1/publisher'
288
- }
289
- }
290
- },
291
- links: { self: '/api/authors/1' }
292
- },
293
- {
294
- type: 'authors',
295
- id: '2',
296
- attributes: { name: 'Johann (NS)', surname: 'Goethe (NS)' },
297
- relationships: {
298
- reviews: { data: [ { type: 'reviews', id: '1' } ] },
299
- publisher: {
300
- data: { type: 'publishers', id: '2' },
301
- links: {
302
- self: '/api/authors/2/relationships/publisher',
303
- related: '/api/authors/2/publisher'
304
- }
305
- }
306
- },
307
- links: { self: '/api/authors/2' }
308
- }
309
- ],
310
- links: { self: '/api/authors' }
311
- }
312
- ```
313
-
314
- When you fetch the french author after adding the reviews, you only get the review _ids_ and not the full review data. This is expected. To fetch the actual reviews, you will need to `include` them.
315
-
316
- ## Including Polymorphic Records (`include`)
317
-
318
- To retrieve related polymorphic resources (e.g., getting a publisher and including all its reviews, or getting an author and including all their reviews), you'll use the **`include` query parameter** from the "one" side of the polymorphic relationship.
319
-
320
- The output format (simplified vs. non-simplified) will depend on the `simplified` parameter of your `get` or `query` call, or the default `simplifiedApi` setting.
321
-
322
- Leaving the exact same schema definition and the exact same data as above, by making these calls:
323
-
324
- ```javascript
325
- const frenchAuthor1_with_reviews_and_includes_simplified = await api.resources.authors.get({ id: frenchAuthor1_ns.id, queryParams: { include: ['reviews'] } });
326
- const frenchAuthor1_with_reviews_and_includes_non_simplified = await api.resources.authors.get({ id: frenchAuthor1_ns.id, queryParams: { include: ['reviews'] }, simplified: false });
327
-
328
- const french_authors_with_includes_simplified = await api.resources.authors.query({queryParams: { include: ['reviews'] } });
329
- // HTTP: GET /api/authors?include=reviews
330
- // Returns (simplified): [
331
- // { id: '1', name: 'Victor Hugo', surname: 'Hugo', publisher: { id: '1' },
332
- // reviews: [{ id: '2', comment: 'A master storyteller', rating: 5, reviewable: { id: '1', _type: 'authors' } }]
333
- // },
334
- // { id: '2', name: 'Alexandre Dumas', surname: 'Dumas', publisher: { id: '1' }, reviews: [] }
335
- // ]
336
-
337
- const french_authors_with_includes_non_simplified = await api.resources.authors.query({queryParams: { include: ['reviews'] }, simplified: false });
338
- // HTTP: GET /api/authors?include=reviews
339
- // Returns (JSON:API): {
340
- // data: [
341
- // { type: 'authors', id: '1', attributes: { name: 'Victor Hugo' },
342
- // relationships: { reviews: { data: [{ type: 'reviews', id: '2' }] } }
343
- // },
344
- // { type: 'authors', id: '2', attributes: { name: 'Alexandre Dumas' },
345
- // relationships: { reviews: { data: [] } }
346
- // }
347
- // ],
348
- // included: [
349
- // { type: 'reviews', id: '2',
350
- // attributes: { comment: 'A master storyteller', rating: 5, reviewable_type: 'authors', reviewable_id: '1' }
351
- // }
352
- // ]
353
- // }
354
-
355
- console.log('French author with the newly added reviews (simplified):')
356
- console.log(inspect(frenchAuthor1_with_reviews_and_includes_simplified));
357
- console.log('French author with the newly added reviews (non-simplified):')
358
- console.log(inspect(frenchAuthor1_with_reviews_and_includes_non_simplified))
359
-
360
- console.log('French authors with the newly added reviews (simplified):')
361
- console.log(inspect(french_authors_with_includes_simplified));
362
- console.log('French authors with the newly added reviews (non-simplified):')
363
- console.log(inspect(french_authors_with_includes_non_simplified))
364
- ```
365
-
366
- **Expected Output**
367
-
368
- ```text
369
- French author with the newly added reviews (simplified):
370
- {
371
- id: '1',
372
- name: 'Victor (NS)',
373
- surname: 'Hugo (NS)',
374
- reviews_ids: [ '4' ],
375
- reviews: [
376
- {
377
- id: '4',
378
- rating: 5,
379
- comment: 'Hugo is a master storyteller! (NS)',
380
- reviewable: { id: '1', _type: 'authors' }
381
- }
382
- ],
383
- publisher: { id: '1' }
384
- }
385
- French author with the newly added reviews (non-simplified):
386
- {
387
- data: {
388
- type: 'authors',
389
- id: '1',
390
- attributes: { name: 'Victor (NS)', surname: 'Hugo (NS)' },
391
- relationships: {
392
- reviews: {
393
- data: [ { type: 'reviews', id: '4' } ],
394
- links: {
395
- self: '/api/authors/1/relationships/reviews',
396
- related: '/api/authors/1/reviews'
397
- }
398
- },
399
- publisher: {
400
- data: { type: 'publishers', id: '1' },
401
- links: {
402
- self: '/api/authors/1/relationships/publisher',
403
- related: '/api/authors/1/publisher'
404
- }
405
- }
406
- },
407
- links: { self: '/api/authors/1' }
408
- },
409
- included: [
410
- {
411
- type: 'reviews',
412
- id: '4',
413
- attributes: { rating: 5, comment: 'Hugo is a master storyteller! (NS)' },
414
- relationships: { reviewable: { data: { type: 'authors', id: '1' } } },
415
- links: { self: '/api/reviews/4' }
416
- }
417
- ],
418
- links: { self: '/api/authors/1' }
419
- }
420
- French authors with the newly added reviews (simplified):
421
- [
422
- {
423
- id: '1',
424
- name: 'Victor (NS)',
425
- surname: 'Hugo (NS)',
426
- reviews_ids: [ '4' ],
427
- reviews: [
428
- {
429
- id: '4',
430
- rating: 5,
431
- comment: 'Hugo is a master storyteller! (NS)',
432
- reviewable: { id: '1', _type: 'authors' }
433
- }
434
- ],
435
- publisher: { id: '1' }
436
- },
437
- {
438
- id: '2',
439
- name: 'Johann (NS)',
440
- surname: 'Goethe (NS)',
441
- reviews: [
442
- {
443
- id: '1',
444
- rating: 4,
445
- comment: 'Great German author! (Simplified)',
446
- reviewable: { id: '2', _type: 'authors' }
447
- }
448
- ],
449
- publisher: { id: '2' }
450
- }
451
- ]
452
- French authors with the newly added reviews (non-simplified):
453
- {
454
- data: [
455
- {
456
- type: 'authors',
457
- id: '1',
458
- attributes: { name: 'Victor (NS)', surname: 'Hugo (NS)' },
459
- relationships: {
460
- reviews: {
461
- data: [ { type: 'reviews', id: '4' } ],
462
- links: {
463
- self: '/api/authors/1/relationships/reviews',
464
- related: '/api/authors/1/reviews'
465
- }
466
- },
467
- publisher: {
468
- data: { type: 'publishers', id: '1' },
469
- links: {
470
- self: '/api/authors/1/relationships/publisher',
471
- related: '/api/authors/1/publisher'
472
- }
473
- }
474
- },
475
- links: { self: '/api/authors/1' }
476
- },
477
- {
478
- type: 'authors',
479
- id: '2',
480
- attributes: { name: 'Johann (NS)', surname: 'Goethe (NS)' },
481
- relationships: {
482
- reviews: {
483
- data: [ { type: 'reviews', id: '1' } ],
484
- links: {
485
- self: '/api/authors/2/relationships/reviews',
486
- related: '/api/authors/2/reviews'
487
- }
488
- },
489
- publisher: {
490
- data: { type: 'publishers', id: '2' },
491
- links: {
492
- self: '/api/authors/2/relationships/publisher',
493
- related: '/api/authors/2/publisher'
494
- }
495
- }
496
- },
497
- links: { self: '/api/authors/2' }
498
- }
499
- ],
500
- included: [
501
- {
502
- type: 'reviews',
503
- id: '4',
504
- attributes: { rating: 5, comment: 'Hugo is a master storyteller! (NS)' },
505
- relationships: { reviewable: { data: { type: 'authors', id: '1' } } },
506
- links: { self: '/api/reviews/4' }
507
- },
508
- {
509
- type: 'reviews',
510
- id: '1',
511
- attributes: { rating: 4, comment: 'Great German author! (Simplified)' },
512
- relationships: { reviewable: { data: { type: 'authors', id: '2' } } },
513
- links: { self: '/api/reviews/1' }
514
- }
515
- ],
516
- links: { self: '/api/authors?include=reviews' }
517
- }
518
- ```
519
-
520
- When fetching data programmatically in **simplified mode** (which is the default), polymorphic `hasMany` relationships will appear as **arrays of child objects** embedded directly within the parent resource, just like regular `hasMany` relationships.
521
-
522
- When you explicitly request **non-simplified output**, the polymorphic `hasMany` relationships will appear in the **`included` array** at the top level of the JSON:API document. The parent resource's `relationships` object will contain links to the related endpoint but not the full related data itself.
523
-
524
- ## Filtering by Polymorphic Relationships
525
-
526
- Filtering by polymorphic relatiohships has two sides:
527
-
528
- * **Filtering the polymorphic resource itself (e.g., `reviews`):**
529
-
530
- This happens when you want to search reviews, and you want to also search in the reviewed item's information. For example searching for 'Apress' would return all reviews where the publisher name includes `Apress`. this is a very common scenario. This is achieved via cross-table filtering.
531
-
532
- * **Filtering the parent resource (e.g., `publishers` or `authors`) by its polymorphic `hasMany` children (`reviews`)**
533
-
534
- This happens when you want to search in `publishers`, and you want to also search in the publisher's reviews. For exaple searching for "terrible" would return all publishers containing a review with the word `awesome`. This is also a very common scenario.
535
-
536
- ### Search (polymorphic)
537
-
538
- Keeping the data already entered, change the resource definitions to match this:
539
-
540
- ```javascript
541
- // Define publishers resource
542
- // Publishers can have many authors (regular relationship) and many reviews (polymorphic)
543
- await api.addResource('publishers', {
544
- schema: {
545
- name: { type: 'string', required: true, max: 255, search: true, indexed: true},
546
- },
547
- relationships: {
548
- authors: { type: 'hasMany', target: 'authors', foreignKey: 'publisher_id' }, // Regular one-to-many
549
- reviews: { type: 'hasMany', target: 'reviews', via: 'reviewable' } // Polymorphic one-to-many
550
- },
551
- searchSchema: {
552
- // Search publishers by their review fields (reverse polymorphic search)
553
- reviewComment: {
554
- type: 'string',
555
- actualField: 'reviews.comment',
556
- filterOperator: 'like'
557
- },
558
- reviewRating: {
559
- type: 'number',
560
- actualField: 'reviews.rating',
561
- filterOperator: '='
562
- }
563
- }
564
- });
565
- await api.resources.publishers.createKnexTable();
566
-
567
- // Define authors resource
568
- // Authors belong to a publisher (regular relationship) and can have many reviews (polymorphic)
569
- await api.addResource('authors', {
570
- schema: {
571
- name: { type: 'string', required: true, max: 100, search: true },
572
- surname: { type: 'string', required: true, max: 100, search: true },
573
- publisher_id: { type: 'id', belongsTo: 'publishers', as: 'publisher', nullable: true }
574
- },
575
- relationships: {
576
- reviews: { type: 'hasMany', target: 'reviews', via: 'reviewable' } // Polymorphic one-to-many
577
- },
578
- searchSchema: {
579
- // Regular search fields
580
- name: { type: 'string', filterOperator: 'like' },
581
- surname: { type: 'string', filterOperator: 'like' },
582
-
583
- // Cross-table search into regular relationship
584
- publisherName: {
585
- type: 'string',
586
- actualField: 'publishers.name',
587
- filterOperator: 'like'
588
- },
589
-
590
- // Cross-table search into polymorphic relationship (reverse polymorphic search)
591
- // This will find authors by searching their reviews' comments
592
- reviewComment: {
593
- type: 'string',
594
- actualField: 'reviews.comment',
595
- filterOperator: 'like'
596
- }
597
- }
598
- });
599
- await api.resources.authors.createKnexTable();
600
-
601
-
602
- // Define reviews resource with a polymorphic relationship
603
- // A review can belong to either a publisher OR an author (but not both)
604
- // This is achieved using a type field and an ID field
605
- await api.addResource('reviews', {
606
- schema: {
607
- rating: { type: 'number', required: true, min: 1, max: 5, indexed: true },
608
- comment: { type: 'string', max: 500, nullable: true, indexed: true },
609
-
610
- // Polymorphic relationship fields:
611
- reviewable_type: { type: 'string', max: 50, required: true }, // Stores 'publishers' or 'authors'
612
- reviewable_id: { type: 'id', required: true }, // Stores the ID of the publisher or author
613
- },
614
- relationships: {
615
- // Polymorphic belongsTo relationship
616
- reviewable: {
617
- belongsToPolymorphic: {
618
- types: ['publishers', 'authors'], // The possible resource types this review can belong to
619
- typeField: 'reviewable_type', // The field in 'reviews' schema storing the parent's type
620
- idField: 'reviewable_id' // The field in 'reviews' schema storing the parent's ID
621
- },
622
- }
623
- },
624
- searchSchema: {
625
- rating: { type: 'number', filterOperator: '=' },
626
- comment: { type: 'string', filterOperator: 'like' },
627
-
628
- // Polymorphic search field (forward polymorphic search)
629
- // This searches for reviews by their parent's name field, regardless of parent type
630
- reviewableName: {
631
- type: 'string',
632
- polymorphicField: 'reviewable', // Reference the polymorphic relationship
633
- targetFields: {
634
- publishers: 'name', // When reviewable_type='publishers', search publishers.name
635
- authors: 'name' // When reviewable_type='authors', search authors.name
636
- },
637
- filterOperator: 'like'
638
- }
639
- }
640
- });
641
- await api.resources.reviews.createKnexTable();
642
- ```
643
-
644
- The schema definitions above demonstrate two powerful polymorphic search patterns:
645
-
646
- **1. Forward Polymorphic Search** (in reviews.searchSchema)
647
-
648
- The `reviewableName` search field allows searching reviews by their parent's name, regardless of whether the parent is a publisher or author:
649
-
650
- ```javascript
651
- reviewableName: {
652
- type: 'string',
653
- polymorphicField: 'reviewable', // Points to the polymorphic relationship
654
- targetFields: {
655
- publishers: 'name', // When parent is a publisher, search its 'name' field
656
- authors: 'name' // When parent is an author, search its 'name' field
657
- },
658
- filterOperator: 'like'
659
- }
660
- ```
661
-
662
- This generates SQL that dynamically JOINs to different tables based on the `reviewable_type`:
663
- - When `reviewable_type = 'publishers'`, it JOINs to the publishers table
664
- - When `reviewable_type = 'authors'`, it JOINs to the authors table
665
-
666
- **2. Reverse Polymorphic Search** (in authors.searchSchema and publishers.searchSchema)
667
-
668
- The `reviewComment` search field allows finding parents (authors/publishers) by searching their polymorphic children (reviews):
669
-
670
- ```javascript
671
- // In authors.searchSchema:
672
- reviewComment: {
673
- type: 'string',
674
- actualField: 'reviews.comment', // Search in the reviews table
675
- filterOperator: 'like'
676
- }
677
-
678
- // In relationships:
679
- reviews: { type: 'hasMany', target: 'reviews', via: 'reviewable' } // Polymorphic relationship
680
- ```
681
-
682
- This uses the `via` property to indicate a polymorphic hasMany relationship. The system automatically adds the polymorphic constraints:
683
- - For authors: `reviews.reviewable_type = 'authors' AND reviews.reviewable_id = authors.id`
684
- - For publishers: `reviews.reviewable_type = 'publishers' AND reviews.reviewable_id = publishers.id`
685
-
686
- This enables powerful queries like "find all authors who have reviews mentioning 'storyteller'" without manually writing complex JOINs.
687
-
688
- With the same data, run these queries:
689
-
690
- ```javascript
691
- // 1. Forward polymorphic search: Find reviews by their parent's name
692
- // This searches across BOTH publishers and authors tables based on reviewable_type
693
- const reviews_filtered_simplified = await api.resources.reviews.query({ queryParams: { filters: {reviewableName: 'Victor'} }})
694
- // HTTP: GET /api/reviews?filter[reviewableName]=Victor
695
- // Returns: [{ id: '2', comment: 'A master storyteller', rating: 5, reviewable: { id: '1', _type: 'authors' } }]
696
-
697
- const reviews_filtered_non_simplified = await api.resources.reviews.query({queryParams: { filters: {reviewableName: 'Victor'} }, simplified: false })
698
- // HTTP: GET /api/reviews?filter[reviewableName]=Victor
699
- // Returns (JSON:API): {
700
- // data: [{
701
- // type: 'reviews',
702
- // id: '2',
703
- // attributes: { comment: 'A master storyteller', rating: 5 },
704
- // relationships: {
705
- // reviewable: { data: { type: 'authors', id: '1' } }
706
- // }
707
- // }]
708
- // }
709
-
710
- // 2. Reverse polymorphic search: Find parents (authors) by their children's (reviews) fields
711
- // This uses a polymorphic JOIN: reviews.reviewable_type = 'authors' AND reviews.reviewable_id = authors.id
712
- const authors_filtered_simplified = await api.resources.authors.query({queryParams: { filters: {reviewComment: 'storyteller'} }})
713
- // HTTP: GET /api/authors?filter[reviewComment]=storyteller
714
- // Returns: [{ id: '1', name: 'Victor Hugo', surname: 'Hugo', publisher: { id: '1' } }]
715
-
716
- const authors_filtered_non_simplified = await api.resources.authors.query({queryParams: { filters: {reviewComment: 'storyteller'} }, simplified: false })
717
- // HTTP: GET /api/authors?filter[reviewComment]=storyteller
718
- // Returns (JSON:API): {
719
- // data: [{ type: 'authors', id: '1', attributes: { name: 'Victor Hugo' } }]
720
- // }
721
-
722
- // 3. Reverse polymorphic search: Find parents (publishers) by their children's (reviews) fields
723
- // This uses a polymorphic JOIN: reviews.reviewable_type = 'publishers' AND reviews.reviewable_id = publishers.id
724
- const publishers_filtered_simplified = await api.resources.publishers.query({queryParams: { filters: {reviewComment: 'enjoy'} }})
725
- // HTTP: GET /api/publishers?filter[reviewComment]=enjoy
726
- // Returns: [{ id: '1', name: 'French Books Inc.' }]
727
-
728
- const publishers_filtered_non_simplified = await api.resources.publishers.query({queryParams: { filters: {reviewComment: 'enjoy'} }, simplified: false })
729
- // HTTP: GET /api/publishers?filter[reviewComment]=enjoy
730
- // Returns (JSON:API): {
731
- // data: [{ type: 'publishers', id: '1', attributes: { name: 'French Books Inc.' } }]
732
- // }
733
-
734
-
735
- console.log('Reviews FILTERED (simplified):')
736
- console.log(inspect(reviews_filtered_simplified));
737
- console.log('Reviews FILTERED (non-simplified):')
738
- console.log(inspect(reviews_filtered_non_simplified))
739
-
740
- console.log('Authors FILTERED (simplified):')
741
- console.log(inspect(authors_filtered_simplified));
742
- console.log('Authors FILTERED (non-simplified):')
743
- console.log(inspect(authors_filtered_non_simplified))
744
-
745
- console.log('Publishers FILTERED (simplified):')
746
- console.log(inspect(publishers_filtered_simplified));
747
- console.log('Publishers FILTERED (non-simplified):')
748
- console.log(inspect(publishers_filtered_non_simplified))
749
-
750
- ```
751
-
752
- **Expected results**
753
- ```
754
- Reviews FILTERED (simplified):
755
- {
756
- data: [
757
- {
758
- id: '4',
759
- rating: 5,
760
- comment: 'Hugo is a master storyteller! (NS)',
761
- reviewable: { id: '1', _type: 'authors' }
762
- }
763
- ],
764
- meta: {...},
765
- links: {...}
766
- }
767
- Reviews FILTERED (non-simplified):
768
- {
769
- data: [
770
- {
771
- type: 'reviews',
772
- id: '4',
773
- attributes: { rating: 5, comment: 'Hugo is a master storyteller! (NS)' },
774
- relationships: {
775
- reviewable: {
776
- data: { type: 'authors', id: '1' },
777
- links: {
778
- self: '/api/reviews/4/relationships/reviewable',
779
- related: '/api/reviews/4/reviewable'
780
- }
781
- }
782
- },
783
- links: { self: '/api/reviews/4' }
784
- }
785
- ],
786
- links: { self: '/api/reviews?filters[reviewableName]=Victor' }
787
- }
788
- Authors FILTERED (simplified):
789
- {
790
- data: [
791
- {
792
- id: '1',
793
- name: 'Victor (NS)',
794
- surname: 'Hugo (NS)',
795
- reviews: [ { id: '4' } ],
796
- publisher: { id: '1' }
797
- }
798
- ],
799
- meta: {...},
800
- links: {...}
801
- }
802
- Authors FILTERED (non-simplified):
803
- {
804
- data: [
805
- {
806
- type: 'authors',
807
- id: '1',
808
- attributes: { name: 'Victor (NS)', surname: 'Hugo (NS)' },
809
- relationships: {
810
- reviews: { data: [ { type: 'reviews', id: '4' } ] },
811
- publisher: {
812
- data: { type: 'publishers', id: '1' },
813
- links: {
814
- self: '/api/authors/1/relationships/publisher',
815
- related: '/api/authors/1/publisher'
816
- }
817
- }
818
- },
819
- links: { self: '/api/authors/1' }
820
- }
821
- ],
822
- links: { self: '/api/authors?filters[reviewComment]=storyteller' }
823
- }
824
- Publishers FILTERED (simplified):
825
- {
826
- data: [
827
- {
828
- id: '1',
829
- name: 'French Books Inc. (NS)',
830
- authors_ids: [ '1' ],
831
- reviews_ids: [ '2', '3' ]
832
- }
833
- ],
834
- meta: {...},
835
- links: {...}
836
- }
837
- Publishers FILTERED (non-simplified):
838
- {
839
- data: [
840
- {
841
- type: 'publishers',
842
- id: '1',
843
- attributes: { name: 'French Books Inc. (NS)' },
844
- relationships: {
845
- authors: { data: [ { type: 'authors', id: '1' } ] },
846
- reviews: {
847
- data: [
848
- { type: 'reviews', id: '2' },
849
- { type: 'reviews', id: '3' }
850
- ]
851
- }
852
- },
853
- links: { self: '/api/publishers/1' }
854
- }
855
- ],
856
- links: { self: '/api/publishers?filters[reviewComment]=enjoy' }
857
- }
858
- ```
859
-
860
- The output demonstrates how polymorphic search works across both simplified and JSON:API formats:
861
-
862
- **Forward Polymorphic Search Results (reviews filtered by parent name "Victor")**
863
-
864
- The query `filters: {reviewableName: 'Victor'}` found review ID 4, which belongs to author "Victor Hugo". The search worked by:
865
- 1. Checking each review's `reviewable_type` field
866
- 2. When it found `reviewable_type = 'authors'`, it JOINed to the authors table using `reviewable_id = authors.id`
867
- 3. It then searched for `authors.name LIKE '%Victor%'`
868
-
869
- In the simplified format, notice how the polymorphic fields are included:
870
- ```javascript
871
- {
872
- id: '4',
873
- rating: 4,
874
- comment: "Great storyteller (NS)",
875
- reviewable_type: 'authors', // These polymorphic fields are restored
876
- reviewable_id: '1' // in simplified mode after our fix!
877
- }
878
- ```
879
-
880
- **Reverse Polymorphic Search Results**
881
-
882
- 1. **Authors filtered by review comment "storyteller"**: Found author ID 1 (Victor Hugo) because he has review ID 4 containing "storyteller"
883
- 2. **Publishers filtered by review comment "enjoy"**: Found publisher ID 1 because it has reviews IDs 2 and 3 containing "enjoy"
884
-
885
- The reverse search worked by:
886
- - JOINing from authors/publishers to reviews with polymorphic constraints
887
- - For authors: `JOIN reviews ON reviews.reviewable_type = 'authors' AND reviews.reviewable_id = authors.id`
888
- - Then filtering: `WHERE reviews.comment LIKE '%storyteller%'`
889
-
890
- In simplified format, the relationships are represented as arrays of IDs:
891
- - `reviews_ids: [ '4' ]` - Author 1 has one review
892
- - `reviews_ids: [ '2', '3' ]` - Publisher 1 has two reviews
893
-
894
- The polymorphic search seamlessly handles the fact that reviews can belong to different parent types, making it easy to search across these complex relationships without writing custom SQL.
895
-
896
- ---
897
-
898
- [Previous: 2.4 hasMany records](./GUIDE_2_4_HasMany_Records.md) | [Back to Guide](./README.md) | [Next: 2.6 Many to Many Relationships](./GUIDE_2_6_Many_To_Many.md)