json-rest-api 1.0.10 → 1.0.11
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/settings.local.json +9 -1
- package/AGENTS.md +34 -0
- package/CLAUDE.md +26 -1
- package/CRUSH.md +55 -0
- package/README.md +3 -3
- package/TODO.todo +8 -0
- package/docs/API.md +17 -18
- package/docs/COMPARISON.md +18 -13
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +17 -21
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +33 -2
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +61 -7
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +58 -61
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +37 -37
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +103 -87
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +37 -34
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +45 -42
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +3 -3
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +1 -1
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -5
- package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +498 -755
- package/docs/GUIDE/GUIDE_6_Positioning.md +1 -3
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +4 -5
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +15 -6
- package/docs/GUIDE/GUIDE_X_Cors.md +2 -5
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +2 -3
- package/docs/GUIDE/GUIDE_X_Multihome.md +1 -4
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -2
- package/docs/GUIDE/GUIDE_X_URL_Management.md +317 -0
- package/docs/GUIDE/index.md +3 -0
- package/docs/QUICKSTART.md +12 -12
- package/docs/index.md +2 -2
- package/docs/mini-guides/file-uploads-complete.md +0 -1
- package/examples/url-override-example.js +137 -0
- package/index.js +7 -1
- package/package.json +5 -9
- package/plugins/core/bulk-operations-plugin.js +2 -0
- package/plugins/core/connectors/express-plugin.js +43 -24
- package/plugins/core/connectors/lib/request-helpers.js +2 -2
- package/plugins/core/lib/anyapi/anyapi-registry.js +821 -0
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +647 -0
- package/plugins/core/lib/anyapi/schema-utils.js +186 -0
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +113 -0
- package/plugins/core/lib/dbTablesOperations.js +305 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +64 -108
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +21 -27
- package/plugins/core/lib/querying/knex-pagination-helpers.js +36 -20
- package/plugins/core/lib/querying/knex-query-helpers-base.js +49 -14
- package/plugins/core/lib/querying/knex-query-helpers.js +378 -222
- package/plugins/core/lib/querying/knex-relationship-includes.js +262 -87
- package/plugins/core/lib/querying/url-helpers.js +119 -0
- package/plugins/core/lib/querying-writing/compile-schemas.js +36 -28
- package/plugins/core/lib/querying-writing/database-capabilities.js +10 -9
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +9 -3
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +2 -2
- package/plugins/core/lib/querying-writing/payload-validators.js +10 -16
- package/plugins/core/lib/querying-writing/schema-helpers.js +52 -60
- package/plugins/core/lib/querying-writing/scope-validations.js +78 -22
- package/plugins/core/lib/querying-writing/simplified-helpers.js +208 -61
- package/plugins/core/lib/storage/storage-adapter.js +160 -0
- package/plugins/core/lib/writing/relationship-processor.js +4 -11
- package/plugins/core/rest-api-access.js +456 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +2285 -0
- package/plugins/core/rest-api-knex-plugin.js +226 -87
- package/plugins/core/rest-api-multihome-plugin.js +89 -27
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +0 -1
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +1 -5
- package/plugins/core/rest-api-plugin-methods/common.js +46 -10
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +31 -27
- package/plugins/core/rest-api-plugin-methods/delete.js +1 -4
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +13 -1
- package/plugins/core/rest-api-plugin-methods/get-related.js +114 -56
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +4 -3
- package/plugins/core/rest-api-plugin-methods/get.js +2 -5
- package/plugins/core/rest-api-plugin-methods/patch.js +15 -16
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +21 -14
- package/plugins/core/rest-api-plugin-methods/post.js +23 -11
- package/plugins/core/rest-api-plugin-methods/put.js +18 -21
- package/plugins/core/rest-api-plugin-methods/query.js +3 -4
- package/plugins/core/rest-api-plugin-methods/release.js +13 -0
- package/plugins/core/rest-api-plugin.js +3 -4
- package/plugins/core/rest-api-positioning-plugin.js +253 -76
- package/plugins/core/socketio-plugin.js +436 -502
- package/quickTest.js +2 -2
- package/scripts/debug-anyapi-review-filter.mjs +63 -0
- package/scripts/inspect-anyapi-descriptor-country.mjs +19 -0
- package/scripts/inspect-anyapi-descriptor.mjs +19 -0
- package/test-debug.js +59 -0
- package/tests/README.md +7 -1
- package/tests/anyapi-basic.test.js +102 -0
- package/tests/anyapi-cursor-pagination.test.js +144 -0
- package/tests/computed-fields.test.js +27 -12
- package/tests/cors.test.js +5 -6
- package/tests/curl.test.js +1 -1
- package/tests/custom-idproperty-relationships.test.js +37 -29
- package/tests/field-getters.test.js +25 -7
- package/tests/fixtures/api-configs.js +576 -150
- package/tests/helpers/storage-mode.js +65 -0
- package/tests/helpers/test-utils.js +85 -3
- package/tests/include-depth-validation.test.js +35 -1
- package/tests/nested-includes.test.js +130 -103
- package/tests/pagination-cursor-multifield.test.js +17 -13
- package/tests/pagination-enhanced.test.js +19 -24
- package/tests/polymorphic-simple.test.js +9 -9
- package/tests/positioning-advanced.test.js +19 -19
- package/tests/positioning-extended.test.js +25 -25
- package/tests/positioning-multifilter.test.js +62 -62
- package/tests/positioning.test.js +7 -7
- package/tests/queries.test.js +5 -5
- package/tests/query-limits.test.js +77 -92
- package/tests/rest-api-access.test.js +206 -0
- package/tests/searchschema-merge.test.js +182 -0
- package/tests/socketio.test.js +33 -14
- package/tests/virtual-fields.test.js +0 -1
- package/docs/DOCS_ALL.md +0 -21894
- package/plugins/core/jwt-auth-plugin.js +0 -897
- package/plugins/core/lib/jwt-auth-helpers.js +0 -130
- package/tests/jwt-auth.test.js +0 -692
|
@@ -56,7 +56,7 @@ $ curl -i -X POST -H "Content-Type: application/vnd.api+json" -d '{
|
|
|
56
56
|
```text
|
|
57
57
|
HTTP/1.1 204 No Content
|
|
58
58
|
X-Powered-By: Express
|
|
59
|
-
Location: /api/
|
|
59
|
+
Location: /api/countries/2
|
|
60
60
|
ETag: W/"a-bAsFyilMr4Ra1hIU5PyoyFRunpI"
|
|
61
61
|
Date: Wed, 23 Jul 2025 07:54:09 GMT
|
|
62
62
|
Connection: keep-alive
|
|
@@ -75,7 +75,7 @@ Date: Wed, 23 Jul 2025 07:54:12 GMT
|
|
|
75
75
|
Connection: keep-alive
|
|
76
76
|
Keep-Alive: timeout=5
|
|
77
77
|
|
|
78
|
-
{"data":{"type":"countries","id":"2","attributes":{"name":"United Kingdom","code":"UK"},"links":{"self":"/api/
|
|
78
|
+
{"data":{"type":"countries","id":"2","attributes":{"name":"United Kingdom","code":"UK"},"links":{"self":"/api/countries/2"}},"links":{"self":"/api/countries/2"}}
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
Note that in "transport" mode after the POST the record was not returned to the client: instead, a status `204 No Content` was returned. However, the client will be aware of the ID of the newly created record thanks to the `Location` header.
|
|
@@ -294,14 +294,17 @@ await api.resources.countries.createKnexTable()
|
|
|
294
294
|
|
|
295
295
|
Note that the definition above is functionally _identical_ to the one provided a few paragraphs above.
|
|
296
296
|
|
|
297
|
-
**Important: When you define a `searchSchema`, it
|
|
297
|
+
**Important: When you define a `searchSchema`, it takes precedence over fields marked with `search: true` in the main schema.** However, fields marked with `search: true` that are NOT in the explicit `searchSchema` will be automatically added to enable searching. This allows you to:
|
|
298
|
+
- Use `searchSchema` to override specific field behaviors
|
|
299
|
+
- Still benefit from `search: true` markers for additional fields
|
|
300
|
+
- Have the explicit `searchSchema` always win in case of conflicts
|
|
298
301
|
|
|
299
302
|
The `searchSchema` gives you:
|
|
300
303
|
|
|
301
|
-
*
|
|
302
|
-
* No mixing of concerns: Your data schema stays focused on data validation and storage, while searchSchema handles search behavior
|
|
303
|
-
*
|
|
304
|
-
*
|
|
304
|
+
* Complete control where needed: Override specific fields while keeping the convenience of `search: true` for others
|
|
305
|
+
* No mixing of concerns: Your data schema stays focused on data validation and storage, while searchSchema handles complex search behavior
|
|
306
|
+
* Flexibility to extend: Add virtual fields or complex filters via searchSchema while basic fields use `search: true`
|
|
307
|
+
* Best of both worlds: Explicit control for complex cases, automatic setup for simple ones
|
|
305
308
|
|
|
306
309
|
`searchSchema` also gives you the ability to define a search field that will search in multiple fields. For example:
|
|
307
310
|
|
|
@@ -340,6 +343,57 @@ Search for "ge": { data: [
|
|
|
340
343
|
|
|
341
344
|
This common pattern will give you the ability to create "global" search fields that will look in multiple fields.
|
|
342
345
|
|
|
346
|
+
### Merging searchSchema with search:true Fields
|
|
347
|
+
|
|
348
|
+
When you combine `searchSchema` with fields marked `search: true`, the library intelligently merges them:
|
|
349
|
+
|
|
350
|
+
```javascript
|
|
351
|
+
await api.addResource('products', {
|
|
352
|
+
schema: {
|
|
353
|
+
name: { type: 'string', search: true }, // Will be added to searchSchema
|
|
354
|
+
description: { type: 'string', search: true }, // Will be added to searchSchema
|
|
355
|
+
price: { type: 'number', search: true }, // Overridden by searchSchema below
|
|
356
|
+
category_id: { type: 'number' }, // Not searchable
|
|
357
|
+
sku: { type: 'string', search: true } // Will be added to searchSchema
|
|
358
|
+
},
|
|
359
|
+
searchSchema: {
|
|
360
|
+
// Explicit searchSchema entries take precedence
|
|
361
|
+
price: {
|
|
362
|
+
type: 'number',
|
|
363
|
+
filterOperator: 'between' // Overrides the search:true with custom operator
|
|
364
|
+
},
|
|
365
|
+
category_name: { // Virtual field (doesn't exist in schema)
|
|
366
|
+
type: 'string',
|
|
367
|
+
actualField: 'category.name',
|
|
368
|
+
filterOperator: 'like'
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// Result: The effective searchSchema includes:
|
|
374
|
+
// - name (from search:true, uses default '=' operator)
|
|
375
|
+
// - description (from search:true, uses default '=' operator)
|
|
376
|
+
// - sku (from search:true, uses default '=' operator)
|
|
377
|
+
// - price (from explicit searchSchema, uses 'between' operator)
|
|
378
|
+
// - category_name (from explicit searchSchema, virtual field)
|
|
379
|
+
|
|
380
|
+
// You can now search using all these fields:
|
|
381
|
+
const results = await api.resources.products.query({
|
|
382
|
+
queryParams: {
|
|
383
|
+
filters: {
|
|
384
|
+
name: 'Widget', // From search:true
|
|
385
|
+
price: [10, 100], // From searchSchema with 'between'
|
|
386
|
+
category_name: 'Electronics' // Virtual field from searchSchema
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
This merge behavior gives you the best of both worlds:
|
|
393
|
+
- Quick setup with `search: true` for simple fields
|
|
394
|
+
- Precise control with `searchSchema` for complex requirements
|
|
395
|
+
- No need to repeat fields in searchSchema that already have `search: true`
|
|
396
|
+
|
|
343
397
|
### Multi-word Search with AND Logic
|
|
344
398
|
|
|
345
399
|
The `oneOf` search feature becomes even more powerful when combined with `splitBy` and `matchAll` options. This allows you to search for multiple words where ALL must appear somewhere in the specified fields.
|
|
@@ -30,23 +30,22 @@ await api.addResource('publishers', {
|
|
|
30
30
|
await api.resources.publishers.createKnexTable();
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
Now, let's add some data.
|
|
33
|
+
Now, let's add some data. Use the relationship name `country` when linking a publisher to a country in simplified mode.
|
|
34
34
|
|
|
35
35
|
```javascript
|
|
36
36
|
const france = await api.resources.countries.post({ name: 'France', code: 'FR' });
|
|
37
37
|
const germany = await api.resources.countries.post({ name: 'Germany', code: 'DE' });
|
|
38
38
|
const uk = await api.resources.countries.post({ name: 'United Kingdom', code: 'UK' });
|
|
39
39
|
|
|
40
|
-
// Create
|
|
40
|
+
// Create publishers linking via the relationship name (simplified syntax)
|
|
41
41
|
const frenchPublisher = await api.resources.publishers.post({
|
|
42
42
|
name: 'French Books Inc.',
|
|
43
43
|
country: france.id
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
-
// Create a publisher linking via the foreign key field directly
|
|
47
46
|
const germanPublisher = await api.resources.publishers.post({
|
|
48
47
|
name: 'German Press GmbH',
|
|
49
|
-
|
|
48
|
+
country: germany.id
|
|
50
49
|
});
|
|
51
50
|
|
|
52
51
|
const ukPublisher = await api.resources.publishers.post({
|
|
@@ -56,7 +55,7 @@ const ukPublisher = await api.resources.publishers.post({
|
|
|
56
55
|
|
|
57
56
|
const internationalPublisher = await api.resources.publishers.post({
|
|
58
57
|
name: 'Global Publishing',
|
|
59
|
-
|
|
58
|
+
country: null
|
|
60
59
|
});
|
|
61
60
|
|
|
62
61
|
|
|
@@ -66,22 +65,29 @@ console.log('Added UK Publisher:', inspect(ukPublisher));
|
|
|
66
65
|
console.log('Added International Publisher:', inspect(internationalPublisher));
|
|
67
66
|
```
|
|
68
67
|
|
|
69
|
-
**
|
|
68
|
+
**Using Relationship Names:**
|
|
70
69
|
|
|
71
|
-
When defining a `belongsTo` relationship with an `as` alias (e.g., `country_id: { ..., as: 'country' }`),
|
|
70
|
+
When defining a `belongsTo` relationship with an `as` alias (e.g., `country_id: { ..., as: 'country' }`), use the relationship name `country` when providing the related resource's ID during `post`, `put`, or `patch` operations in **simplified mode**:
|
|
72
71
|
|
|
73
|
-
*
|
|
74
|
-
*
|
|
72
|
+
* Use the **relationship name** (the `as` value) directly with the ID of the related resource (e.g., `country: france.id`). This provides clarity and aligns with the relationship concept.
|
|
73
|
+
* Foreign key field names (e.g., `country_id`) are no longer supported for relationship input to maintain consistency across all relationship types.
|
|
75
74
|
|
|
76
|
-
|
|
75
|
+
**Field Visibility and Database-First Design:**
|
|
76
|
+
|
|
77
|
+
Note that while the schema defines `country_id` (the actual database column), the API layer abstracts this away:
|
|
78
|
+
- **API Input**: Use `country` (the relationship name)
|
|
79
|
+
- **API Output**: Returns `country: { id: '1' }` (relationship object)
|
|
80
|
+
- **Internal/Hooks**: Work with `country_id` (actual database field via `context.belongsToUpdates`)
|
|
81
|
+
|
|
82
|
+
This separation ensures API consumers never see database implementation details like foreign key fields, while backend developers retain full control over the database structure.
|
|
77
83
|
|
|
78
84
|
**Expected Output (Illustrative, IDs may vary):**
|
|
79
85
|
|
|
80
86
|
```text
|
|
81
|
-
Added French Publisher: { id: '1', name: 'French Books Inc.',
|
|
82
|
-
Added German Publisher: { id: '2', name: 'German Press GmbH',
|
|
83
|
-
Added UK Publisher: { id: '3', name: 'UK Books Ltd.',
|
|
84
|
-
Added International Publisher: { id: '4', name: 'Global Publishing',
|
|
87
|
+
Added French Publisher: { id: '1', name: 'French Books Inc.', country: { id: '1' } }
|
|
88
|
+
Added German Publisher: { id: '2', name: 'German Press GmbH', country: { id: '2' } }
|
|
89
|
+
Added UK Publisher: { id: '3', name: 'UK Books Ltd.', country: { id: '3' } }
|
|
90
|
+
Added International Publisher: { id: '4', name: 'Global Publishing', country: null }
|
|
85
91
|
```
|
|
86
92
|
|
|
87
93
|
## Including `belongsTo` Records (`include`)
|
|
@@ -101,8 +107,8 @@ const uk = await api.resources.countries.post({ name: 'United Kingdom', code: 'U
|
|
|
101
107
|
await api.resources.publishers.post({ name: 'French Books Inc.', country: france.id });
|
|
102
108
|
await api.resources.publishers.post({ name: 'Another French Books Inc.', country: france.id });
|
|
103
109
|
await api.resources.publishers.post({ name: 'UK Books Ltd.', country: uk.id });
|
|
104
|
-
await api.resources.publishers.post({ name: 'German Press GmbH',
|
|
105
|
-
await api.resources.publishers.post({ name: 'Global Publishing',
|
|
110
|
+
await api.resources.publishers.post({ name: 'German Press GmbH', country: germany.id });
|
|
111
|
+
await api.resources.publishers.post({ name: 'Global Publishing', country: null });
|
|
106
112
|
|
|
107
113
|
await api.resources.publishers.post({
|
|
108
114
|
name: 'UK Books Ltd.',
|
|
@@ -127,10 +133,10 @@ const allPublishersWithCountries = await api.resources.publishers.query({
|
|
|
127
133
|
});
|
|
128
134
|
// HTTP: GET /api/publishers?include=country
|
|
129
135
|
// Returns (simplified): [
|
|
130
|
-
// { id: '1', name: 'French Books Inc.',
|
|
131
|
-
// { id: '2', name: 'German Press GmbH',
|
|
132
|
-
// { id: '3', name: 'UK Books Ltd.',
|
|
133
|
-
// { id: '4', name: 'Global Publishing',
|
|
136
|
+
// { id: '1', name: 'French Books Inc.', country: { id: '1', name: 'France', code: 'FR' } },
|
|
137
|
+
// { id: '2', name: 'German Press GmbH', country: { id: '2', name: 'Germany', code: 'DE' } },
|
|
138
|
+
// { id: '3', name: 'UK Books Ltd.', country: { id: '3', name: 'United Kingdom', code: 'UK' } },
|
|
139
|
+
// { id: '4', name: 'Global Publishing', country: null }
|
|
134
140
|
// ]
|
|
135
141
|
|
|
136
142
|
console.log('All Publishers with Countries:', inspect(allPublishersWithCountries));
|
|
@@ -173,7 +179,6 @@ Here is the expected output. Notice how the last call shows the non-simplified v
|
|
|
173
179
|
Publisher with Country: {
|
|
174
180
|
id: '1',
|
|
175
181
|
name: 'French Books Inc.',
|
|
176
|
-
country_id: '1',
|
|
177
182
|
country: { id: '1', name: 'France', code: 'FR' }
|
|
178
183
|
}
|
|
179
184
|
All Publishers with Countries: {
|
|
@@ -181,27 +186,23 @@ All Publishers with Countries: {
|
|
|
181
186
|
{
|
|
182
187
|
id: '1',
|
|
183
188
|
name: 'French Books Inc.',
|
|
184
|
-
country_id: '1',
|
|
185
189
|
country: { id: '1', name: 'France', code: 'FR' }
|
|
186
190
|
},
|
|
187
191
|
{
|
|
188
192
|
id: '2',
|
|
189
193
|
name: 'Another French Books Inc.',
|
|
190
|
-
country_id: '1',
|
|
191
194
|
country: { id: '1', name: 'France', code: 'FR' }
|
|
192
195
|
},
|
|
193
196
|
{
|
|
194
197
|
id: '3',
|
|
195
198
|
name: 'UK Books Ltd.',
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
country: { id: '2', name: 'Germany', code: 'DE' }
|
|
204
|
-
},
|
|
199
|
+
country: { id: '3', name: 'United Kingdom', code: 'UK' }
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: '4',
|
|
203
|
+
name: 'German Press GmbH',
|
|
204
|
+
country: { id: '2', name: 'Germany', code: 'DE' }
|
|
205
|
+
},
|
|
205
206
|
{ id: '5', name: 'Global Publishing' }
|
|
206
207
|
],
|
|
207
208
|
meta: {...},
|
|
@@ -217,12 +218,12 @@ All Publishers with Countries (not simplified): {
|
|
|
217
218
|
country: {
|
|
218
219
|
data: { type: 'countries', id: '1' },
|
|
219
220
|
links: {
|
|
220
|
-
self: '/api/
|
|
221
|
-
related: '/api/
|
|
221
|
+
self: '/api/publishers/1/relationships/country',
|
|
222
|
+
related: '/api/publishers/1/country'
|
|
222
223
|
}
|
|
223
224
|
}
|
|
224
225
|
},
|
|
225
|
-
links: { self: '/api/
|
|
226
|
+
links: { self: '/api/publishers/1' }
|
|
226
227
|
},
|
|
227
228
|
{
|
|
228
229
|
type: 'publishers',
|
|
@@ -232,12 +233,12 @@ All Publishers with Countries (not simplified): {
|
|
|
232
233
|
country: {
|
|
233
234
|
data: { type: 'countries', id: '1' },
|
|
234
235
|
links: {
|
|
235
|
-
self: '/api/
|
|
236
|
-
related: '/api/
|
|
236
|
+
self: '/api/publishers/2/relationships/country',
|
|
237
|
+
related: '/api/publishers/2/country'
|
|
237
238
|
}
|
|
238
239
|
}
|
|
239
240
|
},
|
|
240
|
-
links: { self: '/api/
|
|
241
|
+
links: { self: '/api/publishers/2' }
|
|
241
242
|
},
|
|
242
243
|
{
|
|
243
244
|
type: 'publishers',
|
|
@@ -247,12 +248,12 @@ All Publishers with Countries (not simplified): {
|
|
|
247
248
|
country: {
|
|
248
249
|
data: { type: 'countries', id: '3' },
|
|
249
250
|
links: {
|
|
250
|
-
self: '/api/
|
|
251
|
-
related: '/api/
|
|
251
|
+
self: '/api/publishers/3/relationships/country',
|
|
252
|
+
related: '/api/publishers/3/country'
|
|
252
253
|
}
|
|
253
254
|
}
|
|
254
255
|
},
|
|
255
|
-
links: { self: '/api/
|
|
256
|
+
links: { self: '/api/publishers/3' }
|
|
256
257
|
},
|
|
257
258
|
{
|
|
258
259
|
type: 'publishers',
|
|
@@ -262,11 +263,11 @@ All Publishers with Countries (not simplified): {
|
|
|
262
263
|
country: {
|
|
263
264
|
data: { type: 'countries', id: '2' },
|
|
264
265
|
links: {
|
|
265
|
-
self: '/api/
|
|
266
|
-
related: '/api/
|
|
266
|
+
self: '/api/publishers/4/relationships/country',
|
|
267
|
+
related: '/api/publishers/4/country'
|
|
267
268
|
}
|
|
268
269
|
},
|
|
269
|
-
links: { self: '/api/
|
|
270
|
+
links: { self: '/api/publishers/4' }
|
|
270
271
|
},
|
|
271
272
|
{
|
|
272
273
|
type: 'publishers',
|
|
@@ -276,12 +277,12 @@ All Publishers with Countries (not simplified): {
|
|
|
276
277
|
country: {
|
|
277
278
|
data: null,
|
|
278
279
|
links: {
|
|
279
|
-
self: '/api/
|
|
280
|
-
related: '/api/
|
|
280
|
+
self: '/api/publishers/5/relationships/country',
|
|
281
|
+
related: '/api/publishers/5/country'
|
|
281
282
|
}
|
|
282
283
|
}
|
|
283
284
|
},
|
|
284
|
-
links: { self: '/api/
|
|
285
|
+
links: { self: '/api/publishers/5' }
|
|
285
286
|
}
|
|
286
287
|
],
|
|
287
288
|
included: [
|
|
@@ -290,24 +291,24 @@ All Publishers with Countries (not simplified): {
|
|
|
290
291
|
id: '1',
|
|
291
292
|
attributes: { name: 'France', code: 'FR' },
|
|
292
293
|
relationships: {},
|
|
293
|
-
links: { self: '/api/
|
|
294
|
+
links: { self: '/api/countries/1' }
|
|
294
295
|
},
|
|
295
296
|
{
|
|
296
297
|
type: 'countries',
|
|
297
298
|
id: '3',
|
|
298
299
|
attributes: { name: 'United Kingdom', code: 'UK' },
|
|
299
300
|
relationships: {},
|
|
300
|
-
links: { self: '/api/
|
|
301
|
+
links: { self: '/api/countries/3' }
|
|
301
302
|
},
|
|
302
303
|
{
|
|
303
304
|
type: 'countries',
|
|
304
305
|
id: '2',
|
|
305
306
|
attributes: { name: 'Germany', code: 'DE' },
|
|
306
307
|
relationships: {},
|
|
307
|
-
links: { self: '/api/
|
|
308
|
+
links: { self: '/api/countries/2' }
|
|
308
309
|
}
|
|
309
310
|
],
|
|
310
|
-
links: { self: '/api/
|
|
311
|
+
links: { self: '/api/publishers?include=country' }
|
|
311
312
|
}
|
|
312
313
|
```
|
|
313
314
|
|
|
@@ -328,8 +329,8 @@ const uk = await api.resources.countries.post({ name: 'United Kingdom', code: 'U
|
|
|
328
329
|
|
|
329
330
|
await api.resources.publishers.post({ name: 'French Books Inc.', country: france.id });
|
|
330
331
|
await api.resources.publishers.post({ name: 'UK Books Ltd.', country: uk.id });
|
|
331
|
-
await api.resources.publishers.post({ name: 'German Press GmbH',
|
|
332
|
-
await api.resources.publishers.post({ name: 'Global Publishing',
|
|
332
|
+
await api.resources.publishers.post({ name: 'German Press GmbH', country: germany.id });
|
|
333
|
+
await api.resources.publishers.post({ name: 'Global Publishing', country: null });
|
|
333
334
|
|
|
334
335
|
|
|
335
336
|
// Get a publisher, include its country, but only retrieve publisher name and country code
|
|
@@ -379,7 +380,6 @@ When you specify `fields: { countries: ['code'] }`, this instruction applies to
|
|
|
379
380
|
{
|
|
380
381
|
id: '1',
|
|
381
382
|
name: 'French Books Inc.',
|
|
382
|
-
country_id: '1',
|
|
383
383
|
country: { id: '1', code: 'FR' }
|
|
384
384
|
}
|
|
385
385
|
Sparse Publishers Query (all results): {
|
|
@@ -387,19 +387,16 @@ Sparse Publishers Query (all results): {
|
|
|
387
387
|
{
|
|
388
388
|
id: '1',
|
|
389
389
|
name: 'French Books Inc.',
|
|
390
|
-
country_id: '1',
|
|
391
390
|
country: { id: '1', code: 'FR', name: 'France' }
|
|
392
391
|
},
|
|
393
392
|
{
|
|
394
393
|
id: '2',
|
|
395
394
|
name: 'UK Books Ltd.',
|
|
396
|
-
country_id: '3',
|
|
397
395
|
country: { id: '3', code: 'UK', name: 'United Kingdom' }
|
|
398
396
|
},
|
|
399
397
|
{
|
|
400
398
|
id: '3',
|
|
401
399
|
name: 'German Press GmbH',
|
|
402
|
-
country_id: '2',
|
|
403
400
|
country: { id: '2', code: 'DE', name: 'Germany' }
|
|
404
401
|
},
|
|
405
402
|
{ id: '4', name: 'Global Publishing' }
|
|
@@ -425,8 +422,8 @@ const uk = await api.resources.countries.post({ name: 'United Kingdom', code: 'U
|
|
|
425
422
|
|
|
426
423
|
await api.resources.publishers.post({ name: 'French Books Inc.', country: france.id });
|
|
427
424
|
await api.resources.publishers.post({ name: 'UK Books Ltd.', country: uk.id });
|
|
428
|
-
await api.resources.publishers.post({ name: 'German Press GmbH',
|
|
429
|
-
await api.resources.publishers.post({ name: 'Global Publishing',
|
|
425
|
+
await api.resources.publishers.post({ name: 'German Press GmbH', country: germany.id });
|
|
426
|
+
await api.resources.publishers.post({ name: 'Global Publishing', country: null });
|
|
430
427
|
|
|
431
428
|
|
|
432
429
|
// Programmatic search: Find publishers from France using the country ID alias in searchSchema
|
|
@@ -484,9 +481,9 @@ console.log('Publishers from UK (by countryCode):', inspect(publishersFromUK));
|
|
|
484
481
|
**Expected Output**
|
|
485
482
|
|
|
486
483
|
```text
|
|
487
|
-
Publishers from France (by country ID): [ { id: '1', name: 'French Books Inc.',
|
|
484
|
+
Publishers from France (by country ID): [ { id: '1', name: 'French Books Inc.', country: { id: '1' } } ]
|
|
488
485
|
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.',
|
|
486
|
+
Publishers from UK (by countryCode): [ { id: '2', name: 'UK Books Ltd.', country: { id: '3' } } ]
|
|
490
487
|
```
|
|
491
488
|
|
|
492
489
|
---
|
|
@@ -14,7 +14,7 @@ await api.addResource('publishers', {
|
|
|
14
14
|
},
|
|
15
15
|
relationships: {
|
|
16
16
|
// A publisher has many authors
|
|
17
|
-
authors: { hasMany: 'authors', foreignKey: 'publisher_id' },
|
|
17
|
+
authors: { type: 'hasMany', target: 'authors', foreignKey: 'publisher_id' },
|
|
18
18
|
},
|
|
19
19
|
searchSchema: { // Adding search schema for publishers
|
|
20
20
|
name: { type: 'string', filterOperator: 'like' }
|
|
@@ -80,11 +80,11 @@ console.log('French publisher Authors (author ids only, NOT simplified):', insp
|
|
|
80
80
|
The output:
|
|
81
81
|
|
|
82
82
|
```text
|
|
83
|
-
Added French Publisher: { id: '1', name: 'French Books Inc.',
|
|
84
|
-
Added Victor Hugo: { id: '1', name: 'Victor', surname: 'Hugo',
|
|
85
|
-
Added Émile Zola: { id: '2', name: 'Émile', surname: 'Zola',
|
|
86
|
-
Added German Publisher: { id: '2', name: 'German Press GmbH',
|
|
87
|
-
French publisher Authors (ids only, simplified): { id: '1', name: 'French Books Inc.',
|
|
83
|
+
Added French Publisher: { id: '1', name: 'French Books Inc.', authors: [] }
|
|
84
|
+
Added Victor Hugo: { id: '1', name: 'Victor', surname: 'Hugo', publisher: { id: '1' } }
|
|
85
|
+
Added Émile Zola: { id: '2', name: 'Émile', surname: 'Zola', publisher: { id: '1' } }
|
|
86
|
+
Added German Publisher: { id: '2', name: 'German Press GmbH', authors: [] }
|
|
87
|
+
French publisher Authors (ids only, simplified): { id: '1', name: 'French Books Inc.', authors: [ { id: '1' }, { id: '2' } ] }
|
|
88
88
|
French publisher Authors (ids only, NOT simplified): {
|
|
89
89
|
data: {
|
|
90
90
|
type: 'publishers',
|
|
@@ -95,9 +95,9 @@ French publisher Authors (ids only, NOT simplified): {
|
|
|
95
95
|
data: [ { type: 'authors', id: '1' }, { type: 'authors', id: '2' } ]
|
|
96
96
|
}
|
|
97
97
|
},
|
|
98
|
-
links: { self: '/api/
|
|
98
|
+
links: { self: '/api/publishers/1' }
|
|
99
99
|
},
|
|
100
|
-
links: { self: '/api/
|
|
100
|
+
links: { self: '/api/publishers/1' }
|
|
101
101
|
}
|
|
102
102
|
```
|
|
103
103
|
|
|
@@ -145,12 +145,12 @@ French publisher Authors (with authors, NOT simplified): {
|
|
|
145
145
|
authors: {
|
|
146
146
|
data: [ { type: 'authors', id: '1' }, { type: 'authors', id: '2' } ],
|
|
147
147
|
links: {
|
|
148
|
-
self: '/api/
|
|
149
|
-
related: '/api/
|
|
148
|
+
self: '/api/publishers/1/relationships/authors',
|
|
149
|
+
related: '/api/publishers/1/authors'
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
},
|
|
153
|
-
links: { self: '/api/
|
|
153
|
+
links: { self: '/api/publishers/1' }
|
|
154
154
|
},
|
|
155
155
|
included: [
|
|
156
156
|
{
|
|
@@ -158,17 +158,17 @@ French publisher Authors (with authors, NOT simplified): {
|
|
|
158
158
|
id: '1',
|
|
159
159
|
attributes: { name: 'Victor', surname: 'Hugo' },
|
|
160
160
|
relationships: { publisher: { data: { type: 'publishers', id: '1' } } },
|
|
161
|
-
links: { self: '/api/
|
|
161
|
+
links: { self: '/api/authors/1' }
|
|
162
162
|
},
|
|
163
163
|
{
|
|
164
164
|
type: 'authors',
|
|
165
165
|
id: '2',
|
|
166
166
|
attributes: { name: 'Émile', surname: 'Zola' },
|
|
167
167
|
relationships: { publisher: { data: { type: 'publishers', id: '1' } } },
|
|
168
|
-
links: { self: '/api/
|
|
168
|
+
links: { self: '/api/authors/2' }
|
|
169
169
|
}
|
|
170
170
|
],
|
|
171
|
-
links: { self: '/api/
|
|
171
|
+
links: { self: '/api/publishers/1' }
|
|
172
172
|
}
|
|
173
173
|
```
|
|
174
174
|
|
|
@@ -184,19 +184,19 @@ const allPublishersWithAuthors = await api.resources.publishers.query({
|
|
|
184
184
|
});
|
|
185
185
|
// HTTP: GET /api/publishers?include=authors
|
|
186
186
|
// Returns (simplified): [
|
|
187
|
-
// { id: '1', name: 'French Books Inc.',
|
|
187
|
+
// { id: '1', name: 'French Books Inc.', country: { id: '1' },
|
|
188
188
|
// authors: [
|
|
189
|
-
// { id: '1', name: 'Victor Hugo',
|
|
190
|
-
// { id: '2', name: 'Alexandre Dumas',
|
|
189
|
+
// { id: '1', name: 'Victor Hugo', publisher: { id: '1' } },
|
|
190
|
+
// { id: '2', name: 'Alexandre Dumas', publisher: { id: '1' } }
|
|
191
191
|
// ]
|
|
192
192
|
// },
|
|
193
|
-
// { id: '2', name: 'German Press GmbH',
|
|
193
|
+
// { id: '2', name: 'German Press GmbH', country: { id: '2' },
|
|
194
194
|
// authors: [
|
|
195
|
-
// { id: '3', name: 'Johann Wolfgang von Goethe',
|
|
195
|
+
// { id: '3', name: 'Johann Wolfgang von Goethe', publisher: { id: '2' } }
|
|
196
196
|
// ]
|
|
197
197
|
// },
|
|
198
|
-
// { id: '3', name: 'UK Books Ltd.',
|
|
199
|
-
// { id: '4', name: 'Global Publishing',
|
|
198
|
+
// { id: '3', name: 'UK Books Ltd.', country: { id: '3' }, authors: [] },
|
|
199
|
+
// { id: '4', name: 'Global Publishing', country: null, authors: [] }
|
|
200
200
|
// ]
|
|
201
201
|
|
|
202
202
|
console.log('All Publishers with Authors:', inspect(allPublishersWithAuthors));
|
|
@@ -279,12 +279,12 @@ All Publishers with Authors (not simplified): {
|
|
|
279
279
|
{ type: 'authors', id: '2' }
|
|
280
280
|
],
|
|
281
281
|
links: {
|
|
282
|
-
self: '/api/
|
|
283
|
-
related: '/api/
|
|
282
|
+
self: '/api/publishers/1/relationships/authors',
|
|
283
|
+
related: '/api/publishers/1/authors'
|
|
284
284
|
}
|
|
285
285
|
}
|
|
286
286
|
},
|
|
287
|
-
links: { self: '/api/
|
|
287
|
+
links: { self: '/api/publishers/1' }
|
|
288
288
|
},
|
|
289
289
|
{
|
|
290
290
|
type: 'publishers',
|
|
@@ -294,12 +294,12 @@ All Publishers with Authors (not simplified): {
|
|
|
294
294
|
authors: {
|
|
295
295
|
data: [ { type: 'authors', id: '3' } ],
|
|
296
296
|
links: {
|
|
297
|
-
self: '/api/
|
|
298
|
-
related: '/api/
|
|
297
|
+
self: '/api/publishers/2/relationships/authors',
|
|
298
|
+
related: '/api/publishers/2/authors'
|
|
299
299
|
}
|
|
300
300
|
}
|
|
301
301
|
},
|
|
302
|
-
links: { self: '/api/
|
|
302
|
+
links: { self: '/api/publishers/2' }
|
|
303
303
|
},
|
|
304
304
|
{
|
|
305
305
|
type: 'publishers',
|
|
@@ -309,12 +309,12 @@ All Publishers with Authors (not simplified): {
|
|
|
309
309
|
authors: {
|
|
310
310
|
data: [],
|
|
311
311
|
links: {
|
|
312
|
-
self: '/api/
|
|
313
|
-
related: '/api/
|
|
312
|
+
self: '/api/publishers/3/relationships/authors',
|
|
313
|
+
related: '/api/publishers/3/authors'
|
|
314
314
|
}
|
|
315
315
|
}
|
|
316
316
|
},
|
|
317
|
-
links: { self: '/api/
|
|
317
|
+
links: { self: '/api/publishers/3' }
|
|
318
318
|
}
|
|
319
319
|
],
|
|
320
320
|
included: [
|
|
@@ -323,30 +323,30 @@ All Publishers with Authors (not simplified): {
|
|
|
323
323
|
id: '1',
|
|
324
324
|
attributes: { name: 'Victor', surname: 'Hugo' },
|
|
325
325
|
relationships: { publisher: { data: { type: 'publishers', id: '1' } } },
|
|
326
|
-
links: { self: '/api/
|
|
326
|
+
links: { self: '/api/authors/1' }
|
|
327
327
|
},
|
|
328
328
|
{
|
|
329
329
|
type: 'authors',
|
|
330
330
|
id: '2',
|
|
331
331
|
attributes: { name: 'Émile', surname: 'Zola' },
|
|
332
332
|
relationships: { publisher: { data: { type: 'publishers', id: '1' } } },
|
|
333
|
-
links: { self: '/api/
|
|
333
|
+
links: { self: '/api/authors/2' }
|
|
334
334
|
},
|
|
335
335
|
{
|
|
336
336
|
type: 'authors',
|
|
337
337
|
id: '3',
|
|
338
338
|
attributes: { name: 'Johann', surname: 'Goethe' },
|
|
339
339
|
relationships: { publisher: { data: { type: 'publishers', id: '2' } } },
|
|
340
|
-
links: { self: '/api/
|
|
340
|
+
links: { self: '/api/authors/3' }
|
|
341
341
|
}
|
|
342
342
|
],
|
|
343
|
-
links: { self: '/api/
|
|
343
|
+
links: { self: '/api/publishers?include=authors' }
|
|
344
344
|
}
|
|
345
345
|
```
|
|
346
346
|
|
|
347
347
|
**Important Note on `hasMany` in Non-Simplified Mode:**
|
|
348
348
|
|
|
349
|
-
In non-simplified (full JSON:API) mode, `hasMany` relationships in the `data` section of the parent resource only contain an empty `data` array or `links` to the related endpoint (e.g., `authors: { links: { related: '/api/
|
|
349
|
+
In non-simplified (full JSON:API) mode, `hasMany` relationships in the `data` section of the parent resource only contain an empty `data` array or `links` to the related endpoint (e.g., `authors: { links: { related: '/api/publishers/1/authors' } }`). The actual related `author` resources are placed in the top-level `included` array. This is standard JSON:API behavior to avoid duplicating large amounts of data. The `included` array ensures that each included resource appears only once, even if referenced by multiple parent resources.
|
|
350
350
|
|
|
351
351
|
## Filtering by `hasMany` Relationships
|
|
352
352
|
|
|
@@ -368,7 +368,7 @@ const authorsFromGermanPress = await api.resources.authors.query({
|
|
|
368
368
|
});
|
|
369
369
|
// HTTP: GET /api/authors?filter[publisherName]=German
|
|
370
370
|
// Returns: {
|
|
371
|
-
// data: [{ id: '3', name: 'Johann Wolfgang von Goethe',
|
|
371
|
+
// data: [{ id: '3', name: 'Johann Wolfgang von Goethe', publisher: { id: '2' } }]
|
|
372
372
|
// }
|
|
373
373
|
|
|
374
374
|
console.log('Authors from German Press:', inspect(authorsFromGermanPress));
|
|
@@ -378,7 +378,7 @@ console.log('Authors from German Press:', inspect(authorsFromGermanPress));
|
|
|
378
378
|
The output will be:
|
|
379
379
|
|
|
380
380
|
```text
|
|
381
|
-
Authors from German Press: [ { id: '3', name: 'Johann', surname: 'Goethe',
|
|
381
|
+
Authors from German Press: [ { id: '3', name: 'Johann', surname: 'Goethe', publisher: { id: '2' } } ]
|
|
382
382
|
```
|
|
383
383
|
|
|
384
384
|
Once again, the search logic is always define on the `schema` -- that is, it's handled by the server -- and not by the client.
|