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.
Files changed (117) hide show
  1. package/.claude/settings.local.json +9 -1
  2. package/AGENTS.md +34 -0
  3. package/CLAUDE.md +26 -1
  4. package/CRUSH.md +55 -0
  5. package/README.md +3 -3
  6. package/TODO.todo +8 -0
  7. package/docs/API.md +17 -18
  8. package/docs/COMPARISON.md +18 -13
  9. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +17 -21
  10. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +33 -2
  11. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +61 -7
  12. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +58 -61
  13. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +37 -37
  14. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +103 -87
  15. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +37 -34
  16. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +45 -42
  17. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +3 -3
  18. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +1 -1
  19. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -5
  20. package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +498 -755
  21. package/docs/GUIDE/GUIDE_6_Positioning.md +1 -3
  22. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +4 -5
  23. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +15 -6
  24. package/docs/GUIDE/GUIDE_X_Cors.md +2 -5
  25. package/docs/GUIDE/GUIDE_X_File_Uploads.md +2 -3
  26. package/docs/GUIDE/GUIDE_X_Multihome.md +1 -4
  27. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -2
  28. package/docs/GUIDE/GUIDE_X_URL_Management.md +317 -0
  29. package/docs/GUIDE/index.md +3 -0
  30. package/docs/QUICKSTART.md +12 -12
  31. package/docs/index.md +2 -2
  32. package/docs/mini-guides/file-uploads-complete.md +0 -1
  33. package/examples/url-override-example.js +137 -0
  34. package/index.js +7 -1
  35. package/package.json +5 -9
  36. package/plugins/core/bulk-operations-plugin.js +2 -0
  37. package/plugins/core/connectors/express-plugin.js +43 -24
  38. package/plugins/core/connectors/lib/request-helpers.js +2 -2
  39. package/plugins/core/lib/anyapi/anyapi-registry.js +821 -0
  40. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +647 -0
  41. package/plugins/core/lib/anyapi/schema-utils.js +186 -0
  42. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +113 -0
  43. package/plugins/core/lib/dbTablesOperations.js +305 -0
  44. package/plugins/core/lib/querying/knex-cross-table-search.js +64 -108
  45. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +21 -27
  46. package/plugins/core/lib/querying/knex-pagination-helpers.js +36 -20
  47. package/plugins/core/lib/querying/knex-query-helpers-base.js +49 -14
  48. package/plugins/core/lib/querying/knex-query-helpers.js +378 -222
  49. package/plugins/core/lib/querying/knex-relationship-includes.js +262 -87
  50. package/plugins/core/lib/querying/url-helpers.js +119 -0
  51. package/plugins/core/lib/querying-writing/compile-schemas.js +36 -28
  52. package/plugins/core/lib/querying-writing/database-capabilities.js +10 -9
  53. package/plugins/core/lib/querying-writing/knex-field-helpers.js +9 -3
  54. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +2 -2
  55. package/plugins/core/lib/querying-writing/payload-validators.js +10 -16
  56. package/plugins/core/lib/querying-writing/schema-helpers.js +52 -60
  57. package/plugins/core/lib/querying-writing/scope-validations.js +78 -22
  58. package/plugins/core/lib/querying-writing/simplified-helpers.js +208 -61
  59. package/plugins/core/lib/storage/storage-adapter.js +160 -0
  60. package/plugins/core/lib/writing/relationship-processor.js +4 -11
  61. package/plugins/core/rest-api-access.js +456 -0
  62. package/plugins/core/rest-api-anyapi-knex-plugin.js +2285 -0
  63. package/plugins/core/rest-api-knex-plugin.js +226 -87
  64. package/plugins/core/rest-api-multihome-plugin.js +89 -27
  65. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +0 -1
  66. package/plugins/core/rest-api-plugin-methods/check-permissions.js +1 -5
  67. package/plugins/core/rest-api-plugin-methods/common.js +46 -10
  68. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +31 -27
  69. package/plugins/core/rest-api-plugin-methods/delete.js +1 -4
  70. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +13 -1
  71. package/plugins/core/rest-api-plugin-methods/get-related.js +114 -56
  72. package/plugins/core/rest-api-plugin-methods/get-relationship.js +4 -3
  73. package/plugins/core/rest-api-plugin-methods/get.js +2 -5
  74. package/plugins/core/rest-api-plugin-methods/patch.js +15 -16
  75. package/plugins/core/rest-api-plugin-methods/post-relationship.js +21 -14
  76. package/plugins/core/rest-api-plugin-methods/post.js +23 -11
  77. package/plugins/core/rest-api-plugin-methods/put.js +18 -21
  78. package/plugins/core/rest-api-plugin-methods/query.js +3 -4
  79. package/plugins/core/rest-api-plugin-methods/release.js +13 -0
  80. package/plugins/core/rest-api-plugin.js +3 -4
  81. package/plugins/core/rest-api-positioning-plugin.js +253 -76
  82. package/plugins/core/socketio-plugin.js +436 -502
  83. package/quickTest.js +2 -2
  84. package/scripts/debug-anyapi-review-filter.mjs +63 -0
  85. package/scripts/inspect-anyapi-descriptor-country.mjs +19 -0
  86. package/scripts/inspect-anyapi-descriptor.mjs +19 -0
  87. package/test-debug.js +59 -0
  88. package/tests/README.md +7 -1
  89. package/tests/anyapi-basic.test.js +102 -0
  90. package/tests/anyapi-cursor-pagination.test.js +144 -0
  91. package/tests/computed-fields.test.js +27 -12
  92. package/tests/cors.test.js +5 -6
  93. package/tests/curl.test.js +1 -1
  94. package/tests/custom-idproperty-relationships.test.js +37 -29
  95. package/tests/field-getters.test.js +25 -7
  96. package/tests/fixtures/api-configs.js +576 -150
  97. package/tests/helpers/storage-mode.js +65 -0
  98. package/tests/helpers/test-utils.js +85 -3
  99. package/tests/include-depth-validation.test.js +35 -1
  100. package/tests/nested-includes.test.js +130 -103
  101. package/tests/pagination-cursor-multifield.test.js +17 -13
  102. package/tests/pagination-enhanced.test.js +19 -24
  103. package/tests/polymorphic-simple.test.js +9 -9
  104. package/tests/positioning-advanced.test.js +19 -19
  105. package/tests/positioning-extended.test.js +25 -25
  106. package/tests/positioning-multifilter.test.js +62 -62
  107. package/tests/positioning.test.js +7 -7
  108. package/tests/queries.test.js +5 -5
  109. package/tests/query-limits.test.js +77 -92
  110. package/tests/rest-api-access.test.js +206 -0
  111. package/tests/searchschema-merge.test.js +182 -0
  112. package/tests/socketio.test.js +33 -14
  113. package/tests/virtual-fields.test.js +0 -1
  114. package/docs/DOCS_ALL.md +0 -21894
  115. package/plugins/core/jwt-auth-plugin.js +0 -897
  116. package/plugins/core/lib/jwt-auth-helpers.js +0 -130
  117. 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/1.0/countries/2
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/1.0/countries/2"}},"links":{"self":"/api/1.0/countries/2"}}
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 completely replaces the search configuration from the main schema.** Only fields defined in the `searchSchema` are searchable if `searchSchema` is defined.
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
- * complete control and isolation: With searchSchema, you define the search interface completely separately from the data schema. This can be cleaner when you have complex search requirements.
302
- * No mixing of concerns: Your data schema stays focused on data validation and storage, while searchSchema handles search behavior.
303
- * Easier to see all searchable fields at once: Everything is in one place rather than scattered across field definitions.
304
- * Flexibility to completely diverge from the data schema: You might have 20 fields in your schema but only expose 3 for searching, or create 10 search fields from 3 data fields.
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. 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.
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 a publisher linking via the relationship alias (simplified syntax)
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
- country_id: germany.id
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
- country_id: null
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
- **Explanation of Interchangeability (`country_id` vs. `country`):**
68
+ **Using Relationship Names:**
70
69
 
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**:
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
- * 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.
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
- Both approaches achieve the same result of setting the underlying foreign key in the database.
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.', 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 }
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', country_id: germany.id });
105
- await api.resources.publishers.post({ name: 'Global Publishing', country_id: null });
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.', 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 }
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
- 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
- },
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/1.0/publishers/1/relationships/country',
221
- related: '/api/1.0/publishers/1/country'
221
+ self: '/api/publishers/1/relationships/country',
222
+ related: '/api/publishers/1/country'
222
223
  }
223
224
  }
224
225
  },
225
- links: { self: '/api/1.0/publishers/1' }
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/1.0/publishers/2/relationships/country',
236
- related: '/api/1.0/publishers/2/country'
236
+ self: '/api/publishers/2/relationships/country',
237
+ related: '/api/publishers/2/country'
237
238
  }
238
239
  }
239
240
  },
240
- links: { self: '/api/1.0/publishers/2' }
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/1.0/publishers/3/relationships/country',
251
- related: '/api/1.0/publishers/3/country'
251
+ self: '/api/publishers/3/relationships/country',
252
+ related: '/api/publishers/3/country'
252
253
  }
253
254
  }
254
255
  },
255
- links: { self: '/api/1.0/publishers/3' }
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/1.0/publishers/4/relationships/country',
266
- related: '/api/1.0/publishers/4/country'
266
+ self: '/api/publishers/4/relationships/country',
267
+ related: '/api/publishers/4/country'
267
268
  }
268
269
  },
269
- links: { self: '/api/1.0/publishers/4' }
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/1.0/publishers/5/relationships/country',
280
- related: '/api/1.0/publishers/5/country'
280
+ self: '/api/publishers/5/relationships/country',
281
+ related: '/api/publishers/5/country'
281
282
  }
282
283
  }
283
284
  },
284
- links: { self: '/api/1.0/publishers/5' }
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/1.0/countries/1' }
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/1.0/countries/3' }
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/1.0/countries/2' }
308
+ links: { self: '/api/countries/2' }
308
309
  }
309
310
  ],
310
- links: { self: '/api/1.0/publishers?include=country' }
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', country_id: germany.id });
332
- await api.resources.publishers.post({ name: 'Global Publishing', country_id: null });
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', country_id: germany.id });
429
- await api.resources.publishers.post({ name: 'Global Publishing', country_id: null });
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.', country_id: '1' } ]
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.', country_id: '3' } ]
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.', authors_ids: [] }
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_ids: [] }
87
- French publisher Authors (ids only, simplified): { id: '1', name: 'French Books Inc.', authors_ids: [ '1', '2' ] }
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/1.0/publishers/1' }
98
+ links: { self: '/api/publishers/1' }
99
99
  },
100
- links: { self: '/api/1.0/publishers/1' }
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/1.0/publishers/1/relationships/authors',
149
- related: '/api/1.0/publishers/1/authors'
148
+ self: '/api/publishers/1/relationships/authors',
149
+ related: '/api/publishers/1/authors'
150
150
  }
151
151
  }
152
152
  },
153
- links: { self: '/api/1.0/publishers/1' }
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/1.0/authors/1' }
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/1.0/authors/2' }
168
+ links: { self: '/api/authors/2' }
169
169
  }
170
170
  ],
171
- links: { self: '/api/1.0/publishers/1' }
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.', country_id: '1',
187
+ // { id: '1', name: 'French Books Inc.', country: { id: '1' },
188
188
  // authors: [
189
- // { id: '1', name: 'Victor Hugo', publisher_id: '1' },
190
- // { id: '2', name: 'Alexandre Dumas', publisher_id: '1' }
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', country_id: '2',
193
+ // { id: '2', name: 'German Press GmbH', country: { id: '2' },
194
194
  // authors: [
195
- // { id: '3', name: 'Johann Wolfgang von Goethe', publisher_id: '2' }
195
+ // { id: '3', name: 'Johann Wolfgang von Goethe', publisher: { id: '2' } }
196
196
  // ]
197
197
  // },
198
- // { id: '3', name: 'UK Books Ltd.', country_id: '3', authors: [] },
199
- // { id: '4', name: 'Global Publishing', country_id: null, authors: [] }
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/1.0/publishers/1/relationships/authors',
283
- related: '/api/1.0/publishers/1/authors'
282
+ self: '/api/publishers/1/relationships/authors',
283
+ related: '/api/publishers/1/authors'
284
284
  }
285
285
  }
286
286
  },
287
- links: { self: '/api/1.0/publishers/1' }
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/1.0/publishers/2/relationships/authors',
298
- related: '/api/1.0/publishers/2/authors'
297
+ self: '/api/publishers/2/relationships/authors',
298
+ related: '/api/publishers/2/authors'
299
299
  }
300
300
  }
301
301
  },
302
- links: { self: '/api/1.0/publishers/2' }
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/1.0/publishers/3/relationships/authors',
313
- related: '/api/1.0/publishers/3/authors'
312
+ self: '/api/publishers/3/relationships/authors',
313
+ related: '/api/publishers/3/authors'
314
314
  }
315
315
  }
316
316
  },
317
- links: { self: '/api/1.0/publishers/3' }
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/1.0/authors/1' }
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/1.0/authors/2' }
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/1.0/authors/3' }
340
+ links: { self: '/api/authors/3' }
341
341
  }
342
342
  ],
343
- links: { self: '/api/1.0/publishers?include=authors' }
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/1.0/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.
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', publisher_id: '2' }]
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', publisher_id: '2' } ]
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.