json-rest-api 1.0.6 → 1.0.8

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 (146) hide show
  1. package/.claude/agents/library-guide-expert.md +29 -0
  2. package/.claude/agents/rest-api-internals-expert.md +53 -0
  3. package/CLAUDE.md +139 -1
  4. package/README.md +2 -5
  5. package/docTest.js +238 -0
  6. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +955 -0
  7. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +54 -0
  8. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +750 -0
  9. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +494 -0
  10. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +398 -0
  11. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +882 -0
  12. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +709 -0
  13. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +1233 -0
  14. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +796 -0
  15. package/{README_X_Relationship_Plugin.md → docs/GUIDE/GUIDE_2_9_Relationships_Urls.md} +1 -4
  16. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +66 -0
  17. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +1246 -0
  18. package/docs/GUIDE/GUIDE_5_Non-Db_Stores.md +0 -0
  19. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1490 -0
  20. package/{README_X_SocketIO.md → docs/GUIDE/GUIDE_X_SocketIO.md} +17 -17
  21. package/docs/GUIDE/index.md +58 -0
  22. package/docs/GUIDE.md +61 -2238
  23. package/docs/ONBOARDING/Computed_Properties_And_Sparse_Fields.md +398 -0
  24. package/docs/README.md +57 -2239
  25. package/{README_2_Computed_and_hidden_fields.md → docs/README_VARIOUS/GUIDE_2_Computed_and_hidden_fields.md} +22 -17
  26. package/docs/README_VARIOUS/GUIDE_IN_DOCS.md +2277 -0
  27. package/{README.ORIG.md → docs/README_VARIOUS/README.ORIG.md} +8 -8
  28. package/{README_8_Polymorphic_Relationships.md → docs/README_VARIOUS/README_8_Polymorphic_Relationships.md} +1 -1
  29. package/docs/README_VARIOUS/README_IN_DOCS.ms +2277 -0
  30. package/{RELATIONSHIPS_ADVANCED.md → docs/README_VARIOUS/RELATIONSHIPS_ADVANCED.md} +3 -3
  31. package/{correct-schema-format.md → docs/README_VARIOUS/correct-schema-format.md} +13 -13
  32. package/{pagination.md → docs/README_VARIOUS/pagination.md} +11 -8
  33. package/docs/assets/css/style.scss +78 -5
  34. package/docs/index.md +2 -12
  35. package/helper-reorganization-plan.md +85 -0
  36. package/helper-usage-analysis.md +94 -0
  37. package/package.json +5 -4
  38. package/plugins/core/bulk-operations-plugin.js +2 -2
  39. package/plugins/core/connectors/express-plugin.js +99 -33
  40. package/plugins/core/lib/querying/knex-cross-table-search.js +609 -0
  41. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +491 -0
  42. package/plugins/core/lib/querying/knex-pagination-helpers.js +544 -0
  43. package/plugins/core/lib/{knex-process-includes.js → querying/knex-process-includes.js} +65 -50
  44. package/plugins/core/lib/querying/knex-query-helpers-base.js +74 -0
  45. package/plugins/core/lib/{knex-query-helpers.js → querying/knex-query-helpers.js} +416 -180
  46. package/plugins/core/lib/{knex-relationship-includes.js → querying/knex-relationship-includes.js} +430 -114
  47. package/plugins/core/lib/querying/knex-window-queries.js +328 -0
  48. package/plugins/core/lib/querying-writing/compile-schemas.js +328 -0
  49. package/plugins/core/{utils → lib/querying-writing}/connectors-query-parser.js +62 -57
  50. package/plugins/core/lib/{database-capabilities.js → querying-writing/database-capabilities.js} +113 -7
  51. package/plugins/core/lib/querying-writing/database-value-normalizers.js +369 -0
  52. package/plugins/core/lib/querying-writing/default-data-helpers.js +72 -0
  53. package/plugins/core/{utils → lib/querying-writing}/field-utils.js +41 -31
  54. package/plugins/core/{utils → lib/querying-writing}/knex-constants.js +1 -0
  55. package/plugins/core/lib/{knex-field-helpers.js → querying-writing/knex-field-helpers.js} +190 -57
  56. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +162 -0
  57. package/plugins/core/lib/{payload-validators.js → querying-writing/payload-validators.js} +297 -217
  58. package/plugins/core/lib/querying-writing/schema-helpers.js +497 -0
  59. package/plugins/core/lib/querying-writing/scope-validations.js +431 -0
  60. package/plugins/core/lib/{simplified-helpers.js → querying-writing/simplified-helpers.js} +216 -176
  61. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +110 -0
  62. package/plugins/core/lib/writing/many-to-many-manipulations.js +300 -0
  63. package/plugins/core/lib/writing/relationship-processor.js +252 -0
  64. package/plugins/core/rest-api-knex-plugin.js +183 -62
  65. package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +7 -0
  66. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +72 -0
  67. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +102 -0
  68. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +50 -0
  69. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +39 -0
  70. package/plugins/core/rest-api-plugin-methods/add-route.js +23 -0
  71. package/plugins/core/rest-api-plugin-methods/check-permissions.js +18 -0
  72. package/plugins/core/rest-api-plugin-methods/common.js +605 -0
  73. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +115 -0
  74. package/plugins/core/rest-api-plugin-methods/delete.js +109 -0
  75. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +186 -0
  76. package/plugins/core/rest-api-plugin-methods/get-related.js +295 -0
  77. package/plugins/core/rest-api-plugin-methods/get-relationship.js +62 -0
  78. package/plugins/core/rest-api-plugin-methods/get.js +187 -0
  79. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +56 -0
  80. package/plugins/core/rest-api-plugin-methods/patch.js +193 -0
  81. package/plugins/core/rest-api-plugin-methods/post-relationship.js +104 -0
  82. package/plugins/core/rest-api-plugin-methods/post.js +157 -0
  83. package/plugins/core/rest-api-plugin-methods/put.js +257 -0
  84. package/plugins/core/rest-api-plugin-methods/query.js +208 -0
  85. package/plugins/core/rest-api-plugin.js +108 -2498
  86. package/plugins/core/socketio-plugin.js +28 -5
  87. package/plugins/storage/local-storage.js +2 -2
  88. package/tests/computed-fields.test.js +483 -0
  89. package/tests/cors.test.js +4 -4
  90. package/tests/curl.test.js +5 -4
  91. package/tests/custom-idproperty-relationships.test.js +891 -0
  92. package/tests/field-getters.test.js +353 -0
  93. package/tests/field-setters.test.js +310 -0
  94. package/tests/fixtures/api-configs.js +800 -28
  95. package/tests/jwt-auth.test.js +0 -2
  96. package/tests/nested-includes.test.js +1 -1
  97. package/tests/pagination-cursor-multifield.test.js +514 -0
  98. package/tests/pagination-enhanced.test.js +12 -9
  99. package/tests/polymorphic-relationships.test.js +10 -8
  100. package/tests/polymorphic-simple.test.js +6 -4
  101. package/tests/positioning-advanced.test.js +12 -12
  102. package/tests/positioning-extended.test.js +25 -25
  103. package/tests/positioning-multifilter.test.js +24 -24
  104. package/tests/positioning.test.js +3 -3
  105. package/tests/query-limits.test.js +24 -8
  106. package/tests/relationship-endpoints.test.js +4 -5
  107. package/tests/relationships.test.js +0 -1
  108. package/tests/socketio.test.js +12 -1
  109. package/tests/virtual-fields.test.js +13 -13
  110. package/PLAN_FOR_WEBSOCKETS.md +0 -398
  111. package/README_1_Initial_Setup.md +0 -393
  112. package/README_X_Hooks.md +0 -647
  113. package/TODO_rest-api-plugin-extras.todo +0 -23
  114. package/connectors_ideas/express-plugin.js +0 -120
  115. package/connectors_ideas/http-plugin.js +0 -138
  116. package/docs/examples/express-plugin.js +0 -366
  117. package/example.js +0 -301
  118. package/plugins/core/HELPER_MODULES.md +0 -285
  119. package/plugins/core/lib/compile-schemas.js +0 -245
  120. package/plugins/core/lib/database-value-normalizers.js +0 -192
  121. package/plugins/core/lib/default-data-helpers.js +0 -100
  122. package/plugins/core/lib/knex-cross-table-search.js +0 -442
  123. package/plugins/core/lib/knex-json-api-helpers.js +0 -84
  124. package/plugins/core/lib/knex-json-api-transformers.js +0 -449
  125. package/plugins/core/lib/knex-pagination-helpers.js +0 -193
  126. package/plugins/core/lib/knex-window-queries.js +0 -142
  127. package/plugins/core/lib/many-to-many-manipulations.js +0 -343
  128. package/plugins/core/lib/relationship-processor.js +0 -278
  129. package/plugins/core/lib/schema-helpers.js +0 -343
  130. package/plugins/core/lib/scope-validations.js +0 -252
  131. package/plugins/core/rest-api-relationships-plugin.js +0 -700
  132. package/plugins/core/utils/knex-query-helpers-base.js +0 -56
  133. /package/{README_X_Permissions.md → docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md} +0 -0
  134. /package/{README_X_Positioning.md → docs/GUIDE/GUIDE_6_Positioning.md} +0 -0
  135. /package/{README_X_Bulk_Operations.md → docs/GUIDE/GUIDE_X_Bulk_Operations.md} +0 -0
  136. /package/{README_X_Cors.md → docs/GUIDE/GUIDE_X_Cors.md} +0 -0
  137. /package/{README_X_Multihome.md → docs/GUIDE/GUIDE_X_Multihome.md} +0 -0
  138. /package/{README_X_Appendices.md → docs/GUIDE/GUIDE_Y_Appendices.md} +0 -0
  139. /package/{QUERY_DEPTH_PROTECTION.md → docs/README_VARIOUS/QUERY_DEPTH_PROTECTION.md} +0 -0
  140. /package/{RELATIONSHIPS.md → docs/README_VARIOUS/RELATIONSHIPS.md} +0 -0
  141. /package/{WEBSOCKET_IMPLEMENTATION_STATUS.md → docs/README_VARIOUS/WEBSOCKET_IMPLEMENTATION_STATUS.md} +0 -0
  142. /package/{helper-functions-chart.md → docs/README_VARIOUS/helper-functions-chart.md} +0 -0
  143. /package/docs/{implementation-hidden-computed-fields.md → README_VARIOUS/implementation-hidden-computed-fields.md} +0 -0
  144. /package/docs/{include-limits-migration-guide.md → README_VARIOUS/include-limits-migration-guide.md} +0 -0
  145. /package/{readmeSnippets → docs/README_VARIOUS}/test_put_behavior.js +0 -0
  146. /package/{readmeSnippets → docs/README_VARIOUS}/test_put_fields_without_defaults.js +0 -0
@@ -0,0 +1,494 @@
1
+ # 2.3 `belongsTo` Relationships
2
+
3
+ `belongsTo` relationships represent a one-to-one or many-to-one association where the current resource "belongs to" another resource. For example, a `book` belongs to an `author`, or an `author` belongs to a `country`. These relationships are typically managed by a **foreign key** on the "belonging" resource's table.
4
+
5
+ Let's expand our schema definitions to include `publishers` and link them to `countries`. These schemas will be defined **once** here and reused throughout this section.
6
+
7
+ ```javascript
8
+ // Define countries resource
9
+ await api.addResource('countries', {
10
+ schema: {
11
+ name: { type: 'string', required: true, max: 100, search: true },
12
+ code: { type: 'string', max: 2, unique: true, search: true, indexed: true },
13
+ }
14
+ });
15
+ await api.resources.countries.createKnexTable();
16
+
17
+ // Define publishers resource
18
+ await api.addResource('publishers', {
19
+ schema: {
20
+ name: { type: 'string', required: true, max: 255 },
21
+ country_id: { type: 'id', belongsTo: 'countries', as: 'country', nullable: true }
22
+ },
23
+ // searchSchema completely defines all filterable fields for this resource
24
+ searchSchema: {
25
+ name: { type: 'string' },
26
+ country: { type: 'id', actualField: 'country_id', nullable: true },
27
+ countryCode: { type: 'string', actualField: 'countries.code' }
28
+ }
29
+ });
30
+ await api.resources.publishers.createKnexTable();
31
+ ```
32
+
33
+ Now, let's add some data. Notice the flexibility of using either the foreign key field `country_id` or the relationship alias `country` when linking a publisher to a country in simplified mode.
34
+
35
+ ```javascript
36
+ const france = await api.resources.countries.post({ name: 'France', code: 'FR' });
37
+ const germany = await api.resources.countries.post({ name: 'Germany', code: 'DE' });
38
+ const uk = await api.resources.countries.post({ name: 'United Kingdom', code: 'UK' });
39
+
40
+ // Create a publisher linking via the relationship alias (simplified syntax)
41
+ const frenchPublisher = await api.resources.publishers.post({
42
+ name: 'French Books Inc.',
43
+ country: france.id
44
+ });
45
+
46
+ // Create a publisher linking via the foreign key field directly
47
+ const germanPublisher = await api.resources.publishers.post({
48
+ name: 'German Press GmbH',
49
+ country_id: germany.id
50
+ });
51
+
52
+ const ukPublisher = await api.resources.publishers.post({
53
+ name: 'UK Books Ltd.',
54
+ country: uk.id
55
+ });
56
+
57
+ const internationalPublisher = await api.resources.publishers.post({
58
+ name: 'Global Publishing',
59
+ country_id: null
60
+ });
61
+
62
+
63
+ console.log('Added French Publisher:', inspect(frenchPublisher));
64
+ console.log('Added German Publisher:', inspect(germanPublisher));
65
+ console.log('Added UK Publisher:', inspect(ukPublisher));
66
+ console.log('Added International Publisher:', inspect(internationalPublisher));
67
+ ```
68
+
69
+ **Explanation of Interchangeability (`country_id` vs. `country`):**
70
+
71
+ When defining a `belongsTo` relationship with an `as` alias (e.g., `country_id: { ..., as: 'country' }`), `json-rest-api` provides flexibility in how you provide the related resource's ID during `post`, `put`, or `patch` operations in **simplified mode**:
72
+
73
+ * You can use the **relationship alias** (the `as` value) directly with the ID of the related resource (e.g., `country: france.id`). This is generally recommended for clarity and aligns with the relationship concept.
74
+ * You can use the direct **foreign key field name** (e.g., `country_id: germany.id`). The system is flexible enough to recognize this as a foreign key and process it correctly.
75
+
76
+ Both approaches achieve the same result of setting the underlying foreign key in the database.
77
+
78
+ **Expected Output (Illustrative, IDs may vary):**
79
+
80
+ ```text
81
+ Added French Publisher: { id: '1', name: 'French Books Inc.', country_id: '1' }
82
+ Added German Publisher: { id: '2', name: 'German Press GmbH', country_id: '2' }
83
+ Added UK Publisher: { id: '3', name: 'UK Books Ltd.', country_id: '3' }
84
+ Added International Publisher: { id: '4', name: 'Global Publishing', country_id: null }
85
+ ```
86
+
87
+ ## Including `belongsTo` Records (`include`)
88
+
89
+ To retrieve related `belongsTo` resources, use the `include` query parameter.
90
+
91
+ When fetching data programmatically, `simplified` mode is `true` by default. This means that instead of a separate `included` array (as in full JSON:API), related `belongsTo` resources are **denormalized and embedded directly** within the main resource's object structure, providing a very convenient and flat data structure for immediate use.
92
+
93
+ ### Programmatic Usage:
94
+
95
+ ```javascript
96
+ // Re-add data for a fresh start (schemas are reused from above)
97
+ const france = await api.resources.countries.post({ name: 'France', code: 'FR' });
98
+ const germany = await api.resources.countries.post({ name: 'Germany', code: 'DE' });
99
+ const uk = await api.resources.countries.post({ name: 'United Kingdom', code: 'UK' });
100
+
101
+ await api.resources.publishers.post({ name: 'French Books Inc.', country: france.id });
102
+ await api.resources.publishers.post({ name: 'Another French Books Inc.', country: france.id });
103
+ await api.resources.publishers.post({ name: 'UK Books Ltd.', country: uk.id });
104
+ await api.resources.publishers.post({ name: 'German Press GmbH', country_id: germany.id });
105
+ await api.resources.publishers.post({ name: 'Global Publishing', country_id: null });
106
+
107
+ await api.resources.publishers.post({
108
+ name: 'UK Books Ltd.',
109
+ country: uk.id
110
+ });
111
+
112
+
113
+ // Get a publisher and include its country (simplified mode output)
114
+ const publisherWithCountry = await api.resources.publishers.get({
115
+ id: '1', // ID of French Books Inc.
116
+ queryParams: {
117
+ include: ['country'] // Use the 'as' alias defined in the schema
118
+ }
119
+ });
120
+ console.log('Publisher with Country:', inspect(publisherWithCountry));
121
+
122
+ // Query all publishers and include their countries (simplified mode output)
123
+ const allPublishersWithCountries = await api.resources.publishers.query({
124
+ queryParams: {
125
+ include: ['country']
126
+ }
127
+ });
128
+ // HTTP: GET /api/publishers?include=country
129
+ // Returns (simplified): [
130
+ // { id: '1', name: 'French Books Inc.', country_id: '1', country: { id: '1', name: 'France', code: 'FR' } },
131
+ // { id: '2', name: 'German Press GmbH', country_id: '2', country: { id: '2', name: 'Germany', code: 'DE' } },
132
+ // { id: '3', name: 'UK Books Ltd.', country_id: '3', country: { id: '3', name: 'United Kingdom', code: 'UK' } },
133
+ // { id: '4', name: 'Global Publishing', country_id: null, country: null }
134
+ // ]
135
+
136
+ console.log('All Publishers with Countries:', inspect(allPublishersWithCountries));
137
+ // Note: allPublishersWithCountries contains { data, meta, links }
138
+
139
+ // Query all publishers and include their countries (JSON:API format)
140
+ const allPublishersWithCountriesNotSimplified = await api.resources.publishers.query({
141
+ queryParams: {
142
+ include: ['country']
143
+ },
144
+ simplified: false
145
+ });
146
+ // HTTP: GET /api/publishers?include=country
147
+ // Returns (JSON:API): {
148
+ // data: [
149
+ // { type: 'publishers', id: '1', attributes: { name: 'French Books Inc.' },
150
+ // relationships: { country: { data: { type: 'countries', id: '1' } } } },
151
+ // { type: 'publishers', id: '2', attributes: { name: 'German Press GmbH' },
152
+ // relationships: { country: { data: { type: 'countries', id: '2' } } } },
153
+ // { type: 'publishers', id: '3', attributes: { name: 'UK Books Ltd.' },
154
+ // relationships: { country: { data: { type: 'countries', id: '3' } } } },
155
+ // { type: 'publishers', id: '4', attributes: { name: 'Global Publishing' },
156
+ // relationships: { country: { data: null } } }
157
+ // ],
158
+ // included: [
159
+ // { type: 'countries', id: '1', attributes: { name: 'France', code: 'FR' } },
160
+ // { type: 'countries', id: '2', attributes: { name: 'Germany', code: 'DE' } },
161
+ // { type: 'countries', id: '3', attributes: { name: 'United Kingdom', code: 'UK' } }
162
+ // ]
163
+ // }
164
+
165
+ console.log('All Publishers with Countries (not simplified):', inspect(allPublishersWithCountriesNotSimplified));
166
+ ```
167
+
168
+ Here is the expected output. Notice how the last call shows the non-simplified version of the response, which is muc more verbose. However, it has one _major_ advantage: it only includes the information about France _once_. It might seem like a small gain here, but when you have complex queries where the `belongsTo` table has a lot of data, the saving is much more evident.
169
+
170
+ **Expected Output**
171
+
172
+ ```text
173
+ Publisher with Country: {
174
+ id: '1',
175
+ name: 'French Books Inc.',
176
+ country_id: '1',
177
+ country: { id: '1', name: 'France', code: 'FR' }
178
+ }
179
+ All Publishers with Countries: {
180
+ data: [
181
+ {
182
+ id: '1',
183
+ name: 'French Books Inc.',
184
+ country_id: '1',
185
+ country: { id: '1', name: 'France', code: 'FR' }
186
+ },
187
+ {
188
+ id: '2',
189
+ name: 'Another French Books Inc.',
190
+ country_id: '1',
191
+ country: { id: '1', name: 'France', code: 'FR' }
192
+ },
193
+ {
194
+ id: '3',
195
+ name: 'UK Books Ltd.',
196
+ country_id: '3',
197
+ country: { id: '3', name: 'United Kingdom', code: 'UK' }
198
+ },
199
+ {
200
+ id: '4',
201
+ name: 'German Press GmbH',
202
+ country_id: '2',
203
+ country: { id: '2', name: 'Germany', code: 'DE' }
204
+ },
205
+ { id: '5', name: 'Global Publishing' }
206
+ ],
207
+ meta: {...},
208
+ links: {...}
209
+ }
210
+ All Publishers with Countries (not simplified): {
211
+ data: [
212
+ {
213
+ type: 'publishers',
214
+ id: '1',
215
+ attributes: { name: 'French Books Inc.' },
216
+ relationships: {
217
+ country: {
218
+ data: { type: 'countries', id: '1' },
219
+ links: {
220
+ self: '/api/1.0/publishers/1/relationships/country',
221
+ related: '/api/1.0/publishers/1/country'
222
+ }
223
+ }
224
+ },
225
+ links: { self: '/api/1.0/publishers/1' }
226
+ },
227
+ {
228
+ type: 'publishers',
229
+ id: '2',
230
+ attributes: { name: 'Another French Books Inc.' },
231
+ relationships: {
232
+ country: {
233
+ data: { type: 'countries', id: '1' },
234
+ links: {
235
+ self: '/api/1.0/publishers/2/relationships/country',
236
+ related: '/api/1.0/publishers/2/country'
237
+ }
238
+ }
239
+ },
240
+ links: { self: '/api/1.0/publishers/2' }
241
+ },
242
+ {
243
+ type: 'publishers',
244
+ id: '3',
245
+ attributes: { name: 'UK Books Ltd.' },
246
+ relationships: {
247
+ country: {
248
+ data: { type: 'countries', id: '3' },
249
+ links: {
250
+ self: '/api/1.0/publishers/3/relationships/country',
251
+ related: '/api/1.0/publishers/3/country'
252
+ }
253
+ }
254
+ },
255
+ links: { self: '/api/1.0/publishers/3' }
256
+ },
257
+ {
258
+ type: 'publishers',
259
+ id: '4',
260
+ attributes: { name: 'German Press GmbH' },
261
+ relationships: {
262
+ country: {
263
+ data: { type: 'countries', id: '2' },
264
+ links: {
265
+ self: '/api/1.0/publishers/4/relationships/country',
266
+ related: '/api/1.0/publishers/4/country'
267
+ }
268
+ },
269
+ links: { self: '/api/1.0/publishers/4' }
270
+ },
271
+ {
272
+ type: 'publishers',
273
+ id: '5',
274
+ attributes: { name: 'Global Publishing' },
275
+ relationships: {
276
+ country: {
277
+ data: null,
278
+ links: {
279
+ self: '/api/1.0/publishers/5/relationships/country',
280
+ related: '/api/1.0/publishers/5/country'
281
+ }
282
+ }
283
+ },
284
+ links: { self: '/api/1.0/publishers/5' }
285
+ }
286
+ ],
287
+ included: [
288
+ {
289
+ type: 'countries',
290
+ id: '1',
291
+ attributes: { name: 'France', code: 'FR' },
292
+ relationships: {},
293
+ links: { self: '/api/1.0/countries/1' }
294
+ },
295
+ {
296
+ type: 'countries',
297
+ id: '3',
298
+ attributes: { name: 'United Kingdom', code: 'UK' },
299
+ relationships: {},
300
+ links: { self: '/api/1.0/countries/3' }
301
+ },
302
+ {
303
+ type: 'countries',
304
+ id: '2',
305
+ attributes: { name: 'Germany', code: 'DE' },
306
+ relationships: {},
307
+ links: { self: '/api/1.0/countries/2' }
308
+ }
309
+ ],
310
+ links: { self: '/api/1.0/publishers?include=country' }
311
+ }
312
+ ```
313
+
314
+ ---
315
+
316
+ ## Sparse Fieldsets with `belongsTo` Relations
317
+
318
+ You can apply **sparse fieldsets** not only to the primary resource but also to the included `belongsTo` resources. This is powerful for fine-tuning your API responses and reducing payload sizes.
319
+
320
+ ### Programmatic Usage:
321
+
322
+ ```javascript
323
+ // Re-add data for a fresh start (schemas are reused from above)
324
+
325
+ const france = await api.resources.countries.post({ name: 'France', code: 'FR' });
326
+ const germany = await api.resources.countries.post({ name: 'Germany', code: 'DE' });
327
+ const uk = await api.resources.countries.post({ name: 'United Kingdom', code: 'UK' });
328
+
329
+ await api.resources.publishers.post({ name: 'French Books Inc.', country: france.id });
330
+ await api.resources.publishers.post({ name: 'UK Books Ltd.', country: uk.id });
331
+ await api.resources.publishers.post({ name: 'German Press GmbH', country_id: germany.id });
332
+ await api.resources.publishers.post({ name: 'Global Publishing', country_id: null });
333
+
334
+
335
+ // Get a publisher, include its country, but only retrieve publisher name and country code
336
+ const sparsePublisher = await api.resources.publishers.get({
337
+ id: '1',
338
+ queryParams: {
339
+ include: ['country'],
340
+ fields: {
341
+ publishers: 'name', // Only name for publishers
342
+ countries: 'code' // Only code for countries
343
+ }
344
+ }
345
+ // simplified: true is default for programmatic fetches
346
+ });
347
+ console.log('Sparse Publisher and Country:', inspect(sparsePublisher));
348
+
349
+
350
+ // Query all publishers, include their countries, but only retrieve publisher name and country code
351
+ const sparsePublishersQuery = await api.resources.publishers.query({
352
+ queryParams: {
353
+ include: ['country'],
354
+ fields: {
355
+ publishers: 'name', // Only name for publishers
356
+ countries: 'code,name' // BOTH code and name for countries
357
+ }
358
+ }
359
+ // simplified: true is default for programmatic fetches
360
+ });
361
+ // HTTP: GET /api/publishers?include=country&fields[publishers]=name&fields[countries]=code,name
362
+ // Returns (simplified): [
363
+ // { id: '1', name: 'French Books Inc.', country: { id: '1', code: 'FR', name: 'France' } },
364
+ // { id: '2', name: 'German Press GmbH', country: { id: '2', code: 'DE', name: 'Germany' } },
365
+ // { id: '3', name: 'UK Books Ltd.', country: { id: '3', code: 'UK', name: 'United Kingdom' } },
366
+ // { id: '4', name: 'Global Publishing', country: null }
367
+ // ]
368
+ console.log('Sparse Publishers Query (all results):', inspect(sparsePublishersQuery));
369
+ ```
370
+
371
+ Note that you can specify multiple fields for countries, and that they need to be comma separated.
372
+
373
+ **Important Note on Sparse Fieldsets for Related Resources:**
374
+ When you specify `fields: { countries: ['code'] }`, this instruction applies to *all* `country` resources present in the API response, whether `country` is the primary resource you are querying directly, or if it's included as a related resource. This ensures consistent data representation across the entire response.
375
+
376
+ **Expected Output (Sparse Publisher and Country - Illustrative, IDs may vary):**
377
+
378
+ ```text
379
+ {
380
+ id: '1',
381
+ name: 'French Books Inc.',
382
+ country_id: '1',
383
+ country: { id: '1', code: 'FR' }
384
+ }
385
+ Sparse Publishers Query (all results): {
386
+ data: [
387
+ {
388
+ id: '1',
389
+ name: 'French Books Inc.',
390
+ country_id: '1',
391
+ country: { id: '1', code: 'FR', name: 'France' }
392
+ },
393
+ {
394
+ id: '2',
395
+ name: 'UK Books Ltd.',
396
+ country_id: '3',
397
+ country: { id: '3', code: 'UK', name: 'United Kingdom' }
398
+ },
399
+ {
400
+ id: '3',
401
+ name: 'German Press GmbH',
402
+ country_id: '2',
403
+ country: { id: '2', code: 'DE', name: 'Germany' }
404
+ },
405
+ { id: '4', name: 'Global Publishing' }
406
+ ],
407
+ meta: {...},
408
+ links: {...}
409
+ }
410
+ ```
411
+
412
+ ## Filtering by `belongsTo` Relationships
413
+
414
+ You can filter resources based on conditions applied to their `belongsTo` relationships. This is achieved by defining filterable fields in the `searchSchema` that map to either the foreign key or fields on the related resource.
415
+
416
+ The `searchSchema` offers a clean way to define filters, abstracting away the underlying database structure and relationship navigation from the client. Clients simply use the filter field names defined in `searchSchema` (e.g., `countryCode` instead of `country.code`).
417
+
418
+ ### Programmatic Usage:
419
+
420
+ ```javascript
421
+ // Re-add data for a fresh start (schemas are reused from above)
422
+ const france = await api.resources.countries.post({ name: 'France', code: 'FR' });
423
+ const germany = await api.resources.countries.post({ name: 'Germany', code: 'DE' });
424
+ const uk = await api.resources.countries.post({ name: 'United Kingdom', code: 'UK' });
425
+
426
+ await api.resources.publishers.post({ name: 'French Books Inc.', country: france.id });
427
+ await api.resources.publishers.post({ name: 'UK Books Ltd.', country: uk.id });
428
+ await api.resources.publishers.post({ name: 'German Press GmbH', country_id: germany.id });
429
+ await api.resources.publishers.post({ name: 'Global Publishing', country_id: null });
430
+
431
+
432
+ // Programmatic search: Find publishers from France using the country ID alias in searchSchema
433
+ const publishersFromFrance = await api.resources.publishers.query({
434
+ queryParams: {
435
+ filters: {
436
+ country: france.id // Using 'country' filter field defined in searchSchema
437
+ }
438
+ }
439
+ // simplified: true is default for programmatic fetches
440
+ });
441
+ // HTTP: GET /api/publishers?filter[country]=1
442
+ // Returns: {
443
+ // data: [{ id: '1', name: 'French Books Inc.', country_id: '1' }]
444
+ // }
445
+
446
+ console.log('Publishers from France (by country ID):', inspect(publishersFromFrance));
447
+ // Note: publishersFromFrance contains { data, meta, links }
448
+ // Note: publishersFromFrance contains { data, meta, links } - access publishersFromFrance.data for the array
449
+
450
+ // Programmatic search: Find publishers with no associated country
451
+ const publishersNoCountry = await api.resources.publishers.query({
452
+ queryParams: {
453
+ filters: {
454
+ country: null // Filtering by null for the 'country' ID filter
455
+ }
456
+ }
457
+ // simplified: true is default for programmatic fetches
458
+ });
459
+ // HTTP: GET /api/publishers?filter[country]=null
460
+ // Returns: {
461
+ // data: [{ id: '4', name: 'Global Publishing', country_id: null }]
462
+ // }
463
+
464
+ console.log('Publishers with No Country (by country ID: null):', inspect(publishersNoCountry));
465
+ // Note: publishersNoCountry contains { data, meta, links }
466
+
467
+ // Programmatic search: Find publishers where the associated country's code is 'UK'
468
+ const publishersFromUK = await api.resources.publishers.query({
469
+ queryParams: {
470
+ filters: {
471
+ countryCode: 'UK' // Using 'countryCode' filter field defined in searchSchema
472
+ }
473
+ }
474
+ // simplified: true is default for programmatic fetches
475
+ });
476
+ // HTTP: GET /api/publishers?filter[countryCode]=UK
477
+ // Returns: {
478
+ // data: [{ id: '3', name: 'UK Books Ltd.', country_id: '3' }]
479
+ // }
480
+
481
+ console.log('Publishers from UK (by countryCode):', inspect(publishersFromUK));
482
+ ```
483
+
484
+ **Expected Output**
485
+
486
+ ```text
487
+ Publishers from France (by country ID): [ { id: '1', name: 'French Books Inc.', country_id: '1' } ]
488
+ Publishers with No Country (by country ID: null): [ { id: '4', name: 'Global Publishing' } ]
489
+ Publishers from UK (by countryCode): [ { id: '2', name: 'UK Books Ltd.', country_id: '3' } ]
490
+ ```
491
+
492
+ ---
493
+
494
+ [Previous: 2.2 Manipulating and searching tables with no relationships](./GUIDE_2_2_Manipulating_And_Searching_Tables.md) | [Back to Guide](./README.md) | [Next: 2.4 hasMany records](./GUIDE_2_4_HasMany_Records.md)