json-rest-api 1.0.7 → 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.
- package/.claude/agents/library-guide-expert.md +29 -0
- package/.claude/agents/rest-api-internals-expert.md +53 -0
- package/CLAUDE.md +139 -1
- package/README.md +2 -5
- package/docTest.js +238 -0
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +955 -0
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +54 -0
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +750 -0
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +494 -0
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +398 -0
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +882 -0
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +709 -0
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +1233 -0
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +796 -0
- package/{README_X_Relationship_Plugin.md → docs/GUIDE/GUIDE_2_9_Relationships_Urls.md} +1 -4
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +66 -0
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +1246 -0
- package/docs/GUIDE/GUIDE_5_Non-Db_Stores.md +0 -0
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1490 -0
- package/{README_X_SocketIO.md → docs/GUIDE/GUIDE_X_SocketIO.md} +17 -17
- package/docs/GUIDE/index.md +58 -0
- package/docs/GUIDE.md +61 -2238
- package/docs/ONBOARDING/Computed_Properties_And_Sparse_Fields.md +398 -0
- package/docs/README.md +57 -2239
- package/{README_2_Computed_and_hidden_fields.md → docs/README_VARIOUS/GUIDE_2_Computed_and_hidden_fields.md} +22 -17
- package/docs/README_VARIOUS/GUIDE_IN_DOCS.md +2277 -0
- package/{README.ORIG.md → docs/README_VARIOUS/README.ORIG.md} +8 -8
- package/{README_8_Polymorphic_Relationships.md → docs/README_VARIOUS/README_8_Polymorphic_Relationships.md} +1 -1
- package/docs/README_VARIOUS/README_IN_DOCS.ms +2277 -0
- package/{RELATIONSHIPS_ADVANCED.md → docs/README_VARIOUS/RELATIONSHIPS_ADVANCED.md} +3 -3
- package/{correct-schema-format.md → docs/README_VARIOUS/correct-schema-format.md} +13 -13
- package/{pagination.md → docs/README_VARIOUS/pagination.md} +11 -8
- package/docs/assets/css/style.scss +78 -5
- package/docs/index.md +2 -12
- package/helper-reorganization-plan.md +85 -0
- package/helper-usage-analysis.md +94 -0
- package/package.json +5 -4
- package/plugins/core/bulk-operations-plugin.js +2 -2
- package/plugins/core/connectors/express-plugin.js +90 -32
- package/plugins/core/lib/querying/knex-cross-table-search.js +609 -0
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +491 -0
- package/plugins/core/lib/querying/knex-pagination-helpers.js +544 -0
- package/plugins/core/lib/{knex-process-includes.js → querying/knex-process-includes.js} +65 -50
- package/plugins/core/lib/querying/knex-query-helpers-base.js +74 -0
- package/plugins/core/lib/{knex-query-helpers.js → querying/knex-query-helpers.js} +416 -180
- package/plugins/core/lib/{knex-relationship-includes.js → querying/knex-relationship-includes.js} +430 -114
- package/plugins/core/lib/querying/knex-window-queries.js +328 -0
- package/plugins/core/lib/querying-writing/compile-schemas.js +328 -0
- package/plugins/core/{utils → lib/querying-writing}/connectors-query-parser.js +62 -57
- package/plugins/core/lib/{database-capabilities.js → querying-writing/database-capabilities.js} +113 -7
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +369 -0
- package/plugins/core/lib/querying-writing/default-data-helpers.js +72 -0
- package/plugins/core/{utils → lib/querying-writing}/field-utils.js +41 -31
- package/plugins/core/{utils → lib/querying-writing}/knex-constants.js +1 -0
- package/plugins/core/lib/{knex-field-helpers.js → querying-writing/knex-field-helpers.js} +190 -57
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +162 -0
- package/plugins/core/lib/{payload-validators.js → querying-writing/payload-validators.js} +297 -217
- package/plugins/core/lib/querying-writing/schema-helpers.js +497 -0
- package/plugins/core/lib/querying-writing/scope-validations.js +431 -0
- package/plugins/core/lib/{simplified-helpers.js → querying-writing/simplified-helpers.js} +216 -176
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +110 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +300 -0
- package/plugins/core/lib/writing/relationship-processor.js +252 -0
- package/plugins/core/rest-api-knex-plugin.js +183 -62
- package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +7 -0
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +72 -0
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +102 -0
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +50 -0
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +39 -0
- package/plugins/core/rest-api-plugin-methods/add-route.js +23 -0
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +18 -0
- package/plugins/core/rest-api-plugin-methods/common.js +605 -0
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +115 -0
- package/plugins/core/rest-api-plugin-methods/delete.js +109 -0
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +186 -0
- package/plugins/core/rest-api-plugin-methods/get-related.js +295 -0
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +62 -0
- package/plugins/core/rest-api-plugin-methods/get.js +187 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +56 -0
- package/plugins/core/rest-api-plugin-methods/patch.js +193 -0
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +104 -0
- package/plugins/core/rest-api-plugin-methods/post.js +157 -0
- package/plugins/core/rest-api-plugin-methods/put.js +257 -0
- package/plugins/core/rest-api-plugin-methods/query.js +208 -0
- package/plugins/core/rest-api-plugin.js +108 -2498
- package/plugins/core/socketio-plugin.js +28 -5
- package/plugins/storage/local-storage.js +2 -2
- package/tests/computed-fields.test.js +483 -0
- package/tests/cors.test.js +4 -4
- package/tests/curl.test.js +5 -4
- package/tests/custom-idproperty-relationships.test.js +891 -0
- package/tests/field-getters.test.js +353 -0
- package/tests/field-setters.test.js +310 -0
- package/tests/fixtures/api-configs.js +800 -28
- package/tests/jwt-auth.test.js +0 -2
- package/tests/nested-includes.test.js +1 -1
- package/tests/pagination-cursor-multifield.test.js +514 -0
- package/tests/pagination-enhanced.test.js +12 -9
- package/tests/polymorphic-relationships.test.js +10 -8
- package/tests/polymorphic-simple.test.js +6 -4
- package/tests/positioning-advanced.test.js +12 -12
- package/tests/positioning-extended.test.js +25 -25
- package/tests/positioning-multifilter.test.js +24 -24
- package/tests/positioning.test.js +3 -3
- package/tests/query-limits.test.js +24 -8
- package/tests/relationship-endpoints.test.js +4 -5
- package/tests/relationships.test.js +0 -1
- package/tests/socketio.test.js +12 -1
- package/tests/virtual-fields.test.js +13 -13
- package/PLAN_FOR_WEBSOCKETS.md +0 -398
- package/README_1_Initial_Setup.md +0 -393
- package/README_X_Hooks.md +0 -647
- package/TODO_rest-api-plugin-extras.todo +0 -23
- package/connectors_ideas/express-plugin.js +0 -120
- package/connectors_ideas/http-plugin.js +0 -138
- package/docs/examples/express-plugin.js +0 -366
- package/example.js +0 -301
- package/plugins/core/HELPER_MODULES.md +0 -285
- package/plugins/core/lib/compile-schemas.js +0 -245
- package/plugins/core/lib/database-value-normalizers.js +0 -192
- package/plugins/core/lib/default-data-helpers.js +0 -100
- package/plugins/core/lib/knex-cross-table-search.js +0 -442
- package/plugins/core/lib/knex-json-api-helpers.js +0 -84
- package/plugins/core/lib/knex-json-api-transformers.js +0 -449
- package/plugins/core/lib/knex-pagination-helpers.js +0 -193
- package/plugins/core/lib/knex-window-queries.js +0 -142
- package/plugins/core/lib/many-to-many-manipulations.js +0 -343
- package/plugins/core/lib/relationship-processor.js +0 -278
- package/plugins/core/lib/schema-helpers.js +0 -343
- package/plugins/core/lib/scope-validations.js +0 -252
- package/plugins/core/rest-api-relationships-plugin.js +0 -700
- package/plugins/core/utils/knex-query-helpers-base.js +0 -56
- /package/{README_X_Permissions.md → docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md} +0 -0
- /package/{README_X_Positioning.md → docs/GUIDE/GUIDE_6_Positioning.md} +0 -0
- /package/{README_X_Bulk_Operations.md → docs/GUIDE/GUIDE_X_Bulk_Operations.md} +0 -0
- /package/{README_X_Cors.md → docs/GUIDE/GUIDE_X_Cors.md} +0 -0
- /package/{README_X_Multihome.md → docs/GUIDE/GUIDE_X_Multihome.md} +0 -0
- /package/{README_X_Appendices.md → docs/GUIDE/GUIDE_Y_Appendices.md} +0 -0
- /package/{QUERY_DEPTH_PROTECTION.md → docs/README_VARIOUS/QUERY_DEPTH_PROTECTION.md} +0 -0
- /package/{RELATIONSHIPS.md → docs/README_VARIOUS/RELATIONSHIPS.md} +0 -0
- /package/{WEBSOCKET_IMPLEMENTATION_STATUS.md → docs/README_VARIOUS/WEBSOCKET_IMPLEMENTATION_STATUS.md} +0 -0
- /package/{helper-functions-chart.md → docs/README_VARIOUS/helper-functions-chart.md} +0 -0
- /package/docs/{implementation-hidden-computed-fields.md → README_VARIOUS/implementation-hidden-computed-fields.md} +0 -0
- /package/docs/{include-limits-migration-guide.md → README_VARIOUS/include-limits-migration-guide.md} +0 -0
- /package/{readmeSnippets → docs/README_VARIOUS}/test_put_behavior.js +0 -0
- /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)
|