json-rest-api 1.0.28 → 2.0.0

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