json-rest-api 1.0.28 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/LICENSE +10 -670
  2. package/LICENSE-GPL-3.0 +674 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +39 -30
  5. package/docs/API.md +746 -1273
  6. package/docs/GUIDE/01-getting-started.md +338 -0
  7. package/docs/GUIDE/02-resources-and-relationships.md +33 -0
  8. package/docs/GUIDE/03-running-example.md +87 -0
  9. package/docs/GUIDE/04-creating-and-querying.md +181 -0
  10. package/docs/GUIDE/05-belongs-to.md +177 -0
  11. package/docs/GUIDE/06-has-many.md +184 -0
  12. package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
  13. package/docs/GUIDE/08-many-to-many.md +184 -0
  14. package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
  15. package/docs/GUIDE/10-put-and-patch.md +174 -0
  16. package/docs/GUIDE/11-relationship-endpoints.md +174 -0
  17. package/docs/GUIDE/12-field-transformations.md +287 -0
  18. package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
  19. package/docs/GUIDE/14-custom-resource-methods.md +125 -0
  20. package/docs/GUIDE/15-query-projections.md +266 -0
  21. package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
  22. package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
  23. package/docs/GUIDE/18-labels.md +86 -0
  24. package/docs/GUIDE/19-managed-transactions.md +179 -0
  25. package/docs/GUIDE/20-transaction-outcomes.md +207 -0
  26. package/docs/GUIDE/21-schema-and-migrations.md +634 -0
  27. package/docs/GUIDE/22-version-field-migrations.md +208 -0
  28. package/docs/GUIDE/23-fastify.md +179 -0
  29. package/docs/GUIDE/24-cors.md +172 -0
  30. package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
  31. package/docs/GUIDE/26-file-uploads.md +278 -0
  32. package/docs/GUIDE/27-bulk-operations.md +277 -0
  33. package/docs/GUIDE/28-socketio.md +325 -0
  34. package/docs/GUIDE/29-writing-plugins.md +278 -0
  35. package/docs/GUIDE/30-backend-capabilities.md +100 -0
  36. package/docs/GUIDE/31-positioning.md +181 -0
  37. package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +90 -31
  38. package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
  39. package/docs/GUIDE/index.md +51 -59
  40. package/docs/QUICKSTART.md +120 -287
  41. package/docs/architecture.md +75 -0
  42. package/docs/contributing.md +178 -0
  43. package/index.d.ts +36 -0
  44. package/index.js +1 -22
  45. package/lib/enhanced-logger.js +91 -85
  46. package/lib/error-context.js +445 -5
  47. package/lib/error-formatter.js +219 -59
  48. package/lib/knex-transaction.js +64 -0
  49. package/lib/logger-types.d.ts +22 -0
  50. package/lib/missing-package.js +4 -0
  51. package/lib/rest-api-errors.js +103 -8
  52. package/lib/runtime/json-rest-api.js +172 -0
  53. package/lib/transaction-types.d.ts +59 -0
  54. package/package.json +97 -27
  55. package/plugins/core/bulk-operations-plugin.js +191 -184
  56. package/plugins/core/connectors/express-plugin.js +148 -219
  57. package/plugins/core/connectors/fastify-plugin.js +125 -82
  58. package/plugins/core/connectors/lib/busboy-detector.js +74 -156
  59. package/plugins/core/connectors/lib/connector-core.js +144 -20
  60. package/plugins/core/connectors/lib/formidable-detector.js +63 -120
  61. package/plugins/core/connectors/lib/http-validators.js +77 -0
  62. package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
  63. package/plugins/core/connectors/lib/request-helpers.js +10 -1
  64. package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
  65. package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
  66. package/plugins/core/file-handling-plugin.js +87 -174
  67. package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
  68. package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
  69. package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
  70. package/plugins/core/lib/anyapi/schema-utils.js +25 -16
  71. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  72. package/plugins/core/lib/dbIntrospection.js +228 -21
  73. package/plugins/core/lib/dbTablesOperations.js +409 -92
  74. package/plugins/core/lib/fractional-positioning.js +12 -48
  75. package/plugins/core/lib/querying/identifier-query.js +19 -0
  76. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  77. package/plugins/core/lib/querying/include-to-many.js +428 -0
  78. package/plugins/core/lib/querying/include-to-one.js +579 -0
  79. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  80. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  81. package/plugins/core/lib/querying/knex-pagination-helpers.js +67 -330
  82. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  83. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  84. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  85. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  86. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  87. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  88. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  89. package/plugins/core/lib/querying/query-constraint.js +27 -0
  90. package/plugins/core/lib/querying/query-field-sort-helpers.js +205 -30
  91. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  92. package/plugins/core/lib/querying/sort-helpers.js +20 -1
  93. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  94. package/plugins/core/lib/querying/url-helpers.js +11 -4
  95. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  96. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  97. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  98. package/plugins/core/lib/querying-writing/database-value-normalizers.js +238 -187
  99. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  100. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  101. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  102. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  103. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  104. package/plugins/core/lib/querying-writing/knex-field-helpers.js +111 -287
  105. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  106. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  107. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  108. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  109. package/plugins/core/lib/querying-writing/request-contracts.js +109 -44
  110. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  111. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  112. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  113. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  114. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  115. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  116. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  117. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  118. package/plugins/core/lib/storage/storage-adapter.js +142 -62
  119. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  120. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  121. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  122. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  123. package/plugins/core/lib/writing/resource-version.js +172 -0
  124. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  125. package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2509
  126. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  127. package/plugins/core/rest-api-cors-plugin.js +52 -205
  128. package/plugins/core/rest-api-knex-plugin.js +45 -1069
  129. package/plugins/core/rest-api-label-plugin.js +25 -38
  130. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  131. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  132. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  133. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  134. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  135. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  136. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  137. package/plugins/core/rest-api-plugin-methods/common.js +291 -385
  138. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  139. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  140. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -145
  141. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  142. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  143. package/plugins/core/rest-api-plugin-methods/get.js +38 -69
  144. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  145. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  146. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  147. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  148. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  149. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  150. package/plugins/core/rest-api-plugin-methods/query.js +39 -82
  151. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  152. package/plugins/core/rest-api-plugin.js +32 -46
  153. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  154. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  155. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  156. package/plugins/core/socketio-plugin.js +364 -344
  157. package/plugins/storage/local-storage.js +40 -36
  158. package/types/autofilter.d.ts +57 -0
  159. package/types/bulk-methods.d.ts +63 -0
  160. package/types/errors.d.ts +118 -0
  161. package/types/fastify-options.d.ts +19 -0
  162. package/types/file-detectors.d.ts +33 -0
  163. package/types/file-storage.d.ts +48 -0
  164. package/types/hook-context.d.ts +180 -0
  165. package/types/plugin-options.d.ts +53 -0
  166. package/types/relationship-methods.d.ts +74 -0
  167. package/types/representations.d.ts +50 -0
  168. package/types/resource-methods.d.ts +95 -0
  169. package/types/resource-schema.d.ts +157 -0
  170. package/types/row-policy.d.ts +31 -0
  171. package/types/runtime.d.ts +132 -0
  172. package/types/transactions.d.ts +21 -0
  173. package/.claude/agents/library-guide-expert.md +0 -30
  174. package/.claude/agents/rest-api-internals-expert.md +0 -54
  175. package/.claude/settings.local.json +0 -70
  176. package/AGENTS.md +0 -34
  177. package/CLAUDE.md +0 -160
  178. package/CRUSH.md +0 -55
  179. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  180. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  181. package/TODO.todo +0 -13
  182. package/connectors_ideas/fastify.js +0 -217
  183. package/connectors_ideas/koa-plugin.js +0 -112
  184. package/docs/.bundle/config +0 -2
  185. package/docs/COMPARISON.md +0 -433
  186. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  187. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  188. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  189. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  190. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  191. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  192. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  193. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1238
  194. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  195. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  196. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  197. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  198. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  199. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  200. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  201. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  202. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  203. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  204. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  205. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  206. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  207. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  208. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  209. package/docs/Gemfile +0 -6
  210. package/docs/Gemfile.lock +0 -308
  211. package/docs/ONBOARDING.md +0 -315
  212. package/docs/_config.yml +0 -36
  213. package/docs/_layouts/default.html +0 -123
  214. package/docs/apple-touch-icon.svg +0 -4
  215. package/docs/assets/css/style.scss +0 -392
  216. package/docs/favicon.svg +0 -4
  217. package/docs/index.md +0 -164
  218. package/docs/mini-guides/file-uploads-complete.md +0 -161
  219. package/eslint.config.js +0 -13
  220. package/examples/url-override-example.js +0 -137
  221. package/fixing_plan.md +0 -112
  222. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  223. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  224. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  225. package/plugins/storage/s3-storage.js +0 -93
  226. package/quickTest.js +0 -136
  227. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  228. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  229. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  230. package/scripts/run-docs.js +0 -133
  231. package/test-debug.js +0 -59
  232. package/tests/README.md +0 -202
  233. package/tests/TEST_TEMPLATE.test.js +0 -178
  234. package/tests/anyapi-basic.test.js +0 -105
  235. package/tests/anyapi-cursor-pagination.test.js +0 -150
  236. package/tests/anyapi-custom-idproperty.test.js +0 -163
  237. package/tests/autofilter.test.js +0 -463
  238. package/tests/bulk-operations.test.js +0 -657
  239. package/tests/computed-fields.test.js +0 -586
  240. package/tests/cors.test.js +0 -834
  241. package/tests/curl.test.js +0 -467
  242. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  243. package/tests/custom-id-simplified-post.test.js +0 -75
  244. package/tests/custom-idproperty-relationships.test.js +0 -932
  245. package/tests/db-introspection.test.js +0 -521
  246. package/tests/db-table-operations.test.js +0 -798
  247. package/tests/fastify-plugin.test.js +0 -184
  248. package/tests/field-getters.test.js +0 -370
  249. package/tests/field-setters.test.js +0 -306
  250. package/tests/file-handling.test.js +0 -118
  251. package/tests/fixtures/api-configs.js +0 -2509
  252. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  253. package/tests/helpers/fake-fastify.js +0 -99
  254. package/tests/helpers/storage-mode.js +0 -65
  255. package/tests/helpers/test-utils.js +0 -203
  256. package/tests/http-connectors-parity.test.js +0 -395
  257. package/tests/id-normalization.test.js +0 -444
  258. package/tests/include-depth-validation.test.js +0 -331
  259. package/tests/includes.test.js +0 -427
  260. package/tests/local-storage.test.js +0 -96
  261. package/tests/logical-id-write-validation.test.js +0 -151
  262. package/tests/nested-includes.test.js +0 -617
  263. package/tests/pagination-cursor-multifield.test.js +0 -643
  264. package/tests/pagination-enhanced.test.js +0 -563
  265. package/tests/polymorphic-relationships.test.js +0 -347
  266. package/tests/polymorphic-simple.test.js +0 -148
  267. package/tests/positioning-advanced.test.js +0 -692
  268. package/tests/positioning-extended.test.js +0 -915
  269. package/tests/positioning-multifilter.test.js +0 -699
  270. package/tests/positioning.test.js +0 -560
  271. package/tests/post-commit-hooks.test.js +0 -120
  272. package/tests/projected-fields.test.js +0 -319
  273. package/tests/public-exports.test.js +0 -15
  274. package/tests/put-create-parity.test.js +0 -216
  275. package/tests/queries.test.js +0 -521
  276. package/tests/query-limits.test.js +0 -584
  277. package/tests/relationship-endpoints.test.js +0 -620
  278. package/tests/relationship-include-storage-adapter.test.js +0 -56
  279. package/tests/relationships.test.js +0 -681
  280. package/tests/rest-api.test.js +0 -337
  281. package/tests/return-record-settings.test.js +0 -184
  282. package/tests/row-policy.test.js +0 -489
  283. package/tests/s3-storage.test.js +0 -38
  284. package/tests/searchschema-knex-behavior.test.js +0 -187
  285. package/tests/searchschema-merge.test.js +0 -151
  286. package/tests/socketio.test.js +0 -860
  287. package/tests/storage-mapping.test.js +0 -362
  288. package/tests/test-development-guide.md +0 -370
  289. package/tests/virtual-fields.test.js +0 -404
@@ -1,808 +0,0 @@
1
- # 2.2 Manipulating and searching tables with no relationships
2
-
3
- ## Setting up the schema
4
-
5
- First of all, define a resource with a schema:
6
-
7
- ```javascript
8
- // Countries table
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 }, // ISO country code
13
- }
14
- });
15
- await api.resources.countries.createKnexTable()
16
- ```
17
-
18
- ## Adding data
19
-
20
- Note that we create the database table at the same time.
21
- This resource has just two fields, name and code, both searchable.
22
-
23
- Programmatically you can `post` and `get` easily:
24
-
25
- ```javascript
26
- const addedFrance = await api.resources.countries.post({ name: 'France', code: 'FR' });
27
- console.log('Added record :', inspect(addedFrance))
28
-
29
- const fetchedFrance = await api.resources.countries.get({ id: addedFrance.id });
30
- console.log('Refetched record:', inspect(fetchedFrance))
31
- ```
32
-
33
- After the logging messages, you will see:
34
-
35
- ```text
36
- Added record : { id: '1', name: 'France', code: 'FR' }
37
- Refetched record: { id: '1', name: 'France', code: 'FR' }
38
- Express server started on port 3000. API available at http://localhost:3000/api
39
- ```
40
-
41
- You can do the same thing talking to the server directly (although you will be dealing with JSON:API results).
42
- Leaving the code as it is, you will add nother country:
43
-
44
-
45
- ```bash
46
- $ curl -i -X POST -H "Content-Type: application/vnd.api+json" -d '{
47
- "data": {
48
- "type": "countries",
49
- "attributes": {
50
- "name": "United Kingdom",
51
- "code": "UK"
52
- }
53
- }
54
- }' http://localhost:3000/api/countries
55
- ```
56
- ```text
57
- HTTP/1.1 204 No Content
58
- X-Powered-By: Express
59
- Location: /api/countries/2
60
- ETag: W/"a-bAsFyilMr4Ra1hIU5PyoyFRunpI"
61
- Date: Wed, 23 Jul 2025 07:54:09 GMT
62
- Connection: keep-alive
63
- Keep-Alive: timeout=5
64
- ```
65
- ```bash
66
- $ curl -i -X GET http://localhost:3000/api/countries/2
67
- ```
68
- ```txt
69
- HTTP/1.1 200 OK
70
- X-Powered-By: Express
71
- Content-Type: application/vnd.api+json; charset=utf-8
72
- Content-Length: 169
73
- ETag: W/"a9-lnEVXaZ/V6qra0YgjpoEBUTZ3EY"
74
- Date: Wed, 23 Jul 2025 07:54:12 GMT
75
- Connection: keep-alive
76
- Keep-Alive: timeout=5
77
-
78
- {"data":{"type":"countries","id":"2","attributes":{"name":"United Kingdom","code":"UK"},"links":{"self":"/api/countries/2"}},"links":{"self":"/api/countries/2"}}
79
- ```
80
-
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.
82
-
83
- ## Manipulating data
84
-
85
- Replace the data commands with these:
86
-
87
- ```javascript
88
- await api.addResource('countries', {
89
- schema: {
90
- id: { type: 'string' },
91
- name: { type: 'string', required: true, max: 100, search: true, filterOperator: 'like' },
92
- code: { type: 'string', max: 2, unique: true, search: true }, // ISO country code
93
- }
94
- });
95
- await api.resources.countries.createKnexTable()
96
-
97
- const fr = await api.resources.countries.post({ name: 'France', code: 'FR' });
98
- const it = await api.resources.countries.post({ name: 'Italyy', code: 'IT' }); // Typo intentional
99
- const de = await api.resources.countries.post({ name: 'Germ', code: 'DE' }); // Typo intentional
100
-
101
- // Patching Germany. It will only change the name
102
- await api.resources.countries.patch({id: de.id, name: 'Germany' })
103
- let deFromDb = await api.resources.countries.get({ id: de.id });
104
-
105
- // Putting France. Note that this will actually reset any attributes that were not passed
106
- await api.resources.countries.put({id: it.id, name: 'Italy' })
107
- let itFromDb = await api.resources.countries.get({ id: it.id });
108
-
109
- console.log('Patched record (Germany):', inspect(deFromDb))
110
- console.log('Put record: (Italy)', inspect(itFromDb))
111
- ```
112
-
113
- The result will be:
114
-
115
- ```text
116
- Patched record (Germany): { id: '3', name: 'Germany', code: 'DE' }
117
- Put record: (Italy) { id: '2', name: 'Italy', code: null }
118
- ```
119
-
120
- As you can see, using PUT on Italy was a problem: since put didn't include the `code` field, and since PUT assumes a FULL record, the `code` field was reset to null. On the other hand, since the method PATCH assumes a partial update, the update for Germany did not _not_ overwrite the `code` field. This is a very important distinction, and it's the reason why most clients avoid PUT calls.
121
-
122
-
123
- ## Search (Filtering)
124
-
125
- The API supports powerful filtering capabilities out of the box for any fields you've marked as `search: true` in your schema.
126
-
127
- **Important Note about Query Results in Simplified Mode:**
128
-
129
- When using `query()` in simplified mode (the default for programmatic access), the return value is an object containing:
130
- - `data`: The array of matching records
131
- - `meta`: Metadata about the results (e.g., pagination info)
132
- - `links`: Links for pagination and related resources
133
-
134
- For example:
135
- ```javascript
136
- const result = await api.resources.countries.query({ /* ... */ });
137
- // result is: { data: [...], meta: {...}, links: {...} }
138
- // To access the records: result.data
139
- ```
140
-
141
- **Programmatic Example: Searching for countries**
142
-
143
- Change the code to add some countries:
144
-
145
- ```javascript
146
- const fr = await api.resources.countries.post({ name: 'France', code: 'FR' });
147
- const it = await api.resources.countries.post({ name: 'Italy', code: 'IT' });
148
- const de = await api.resources.countries.post({ name: 'Germany', code: 'DE' });
149
- const au = await api.resources.countries.post({ name: 'Australia', code: 'AU' });
150
- const at = await api.resources.countries.post({ name: 'Austria', code: 'AT' });
151
- const ge = await api.resources.countries.post({ name: 'Georgia', code: 'GE' });
152
-
153
- const searchAustralia = await api.resources.countries.query({
154
- queryParams: {
155
- filters: {
156
- name: 'Australia'
157
- }
158
- }
159
- });
160
- // HTTP: GET /api/countries?filter[name]=Australia
161
- // Returns: {
162
- // data: [{ id: '2', name: 'Australia', code: 'AU' }]
163
- // }
164
-
165
- const searchAustr = await api.resources.countries.query({
166
- queryParams: {
167
- filters: {
168
- name: 'Austr'
169
- }
170
- }
171
- });
172
- // HTTP: GET /api/countries?filter[name]=Austr
173
- // Returns: {
174
- // data: []
175
- // }
176
-
177
- console.log('Search for "Australia":', inspect(searchAustralia))
178
- console.log('Search for "Austr":', inspect(searchAustr))
179
- // Note: searchAustralia and searchAustr contain { data, meta, links } objects
180
- ```
181
-
182
- The result will be:
183
-
184
- ```
185
- Search for Australia: { data: [ { id: '4', name: 'Australia', code: 'AU' } ], meta: {...}, links: {...} }
186
- Search for "Austr": { data: [], meta: {...}, links: {...} }
187
- ```
188
-
189
- It's clear that the search is only matching precise results.
190
-
191
-
192
- There are two ways to enable search on a field in your schema. The first one is the one we are currently using, with `search: true`.
193
- As seen above, the only time filtering works is when there is an exact match.
194
- However, rather than `true` or `false`, `search` can also be an object.
195
- Changing the definition of `name` in the `countries` resource to this:
196
-
197
- ```javascript
198
- name: { type: 'string', required: true, max: 100, search: { filterOperator: 'like' } },
199
- ```
200
-
201
- Will give you the expected results:
202
-
203
- ```text
204
- Search for Australia: [ { id: '4', name: 'Australia', code: 'AU' } ]
205
- Search for "Austr": [
206
- { id: '4', name: 'Australia', code: 'AU' },
207
- { id: '5', name: 'Austria', code: 'AT' }
208
- ]
209
- ```
210
-
211
- This is the
212
-
213
- **Available operators for `filterOperator`:**
214
- - `'='` - Exact match (default)
215
- - `'like'` - Partial match with % wildcards automatically added on both sides
216
- - `'>'`, `'>='`, `'<'`, `'<='` - Comparison operators for numeric/date fields
217
- - Other SQL operators are passed through directly (e.g., `'!='`, `'<>'`)
218
-
219
- Note: Only `'like'` receives special handling (automatic % wildcards). All other operators are passed directly to the SQL query.
220
-
221
- You can also define multiple search patterns from a single field:
222
-
223
- ```javascript
224
- await api.addResource('countries', {
225
- schema: {
226
- name: {
227
- type: 'string', required: true, search: {
228
- name: { filterOperator: '=', type: 'string' },
229
- nameLike: { filterOperator: 'like', type: 'string' }
230
- }
231
- },
232
- code: { type: 'string', unique: true, search: true }
233
- }
234
- });
235
- await api.resources.countries.createKnexTable()
236
-
237
- await api.resources.countries.post({ name: 'Georgia', code: 'GE' });
238
- await api.resources.countries.post({ name: 'France', code: 'FR' });
239
- await api.resources.countries.post({ name: 'Italy', code: 'IT' });
240
- await api.resources.countries.post({ name: 'Germany', code: 'DE' });
241
- await api.resources.countries.post({ name: 'Australia', code: 'AU' });
242
- await api.resources.countries.post({ name: 'Austria', code: 'AT' });
243
-
244
- const searchAustralia = await api.resources.countries.query({
245
- queryParams: {
246
- filters: {
247
- name: 'Australia'
248
- }
249
- }
250
- });
251
- // HTTP: GET /api/countries?filter[name]=Australia
252
- // Returns: {
253
- // data: [{ id: '2', name: 'Australia', code: 'AU' }]
254
- // }
255
-
256
- const searchAustr = await api.resources.countries.query({
257
- queryParams: {
258
- filters: {
259
- nameLike: 'Austr'
260
- }
261
- }
262
- });
263
- // HTTP: GET /api/countries?filter[nameLike]=Austr
264
- // Returns: {
265
- // data: [{ id: '2', name: 'Australia', code: 'AU' }, { id: '3', name: 'Austria', code: 'AT' }]
266
- // }
267
-
268
- console.log('Search for "Australia":', inspect(searchAustralia))
269
- console.log('Search for "Austr":', inspect(searchAustr))
270
- ```
271
-
272
- This is very powerful in that it allows you to define multiple ways of filtering a field depending on needs.
273
-
274
- There is another, even more powerful way to define how to search in a resource: define a whole searchSchema that is completely independent to the main schema.
275
-
276
- Under the hood, `rest-api-plugin` actually creates a `searchSchema` object based on the option on the default schema. However, it's very possible for an attribute to define a `searchSchema` directly:
277
-
278
-
279
- ```javascript
280
- await api.addResource('countries', {
281
- schema: {
282
- name: { type: 'string', required: true, max: 100 },
283
- code: { type: 'string', max: 2, unique: true }
284
- },
285
- searchSchema: {
286
- // Define searchable fields explicitly
287
- name: { type: 'string', filterOperator: '=' },
288
- code: { type: 'string', filterOperator: '=' },
289
- nameLike: { type: 'string', actualField: 'name', filterOperator: 'like' }
290
- }
291
- });
292
- await api.resources.countries.createKnexTable()
293
- ```
294
-
295
- Note that the definition above is functionally _identical_ to the one provided a few paragraphs above.
296
-
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
301
-
302
- The `searchSchema` gives you:
303
-
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
308
-
309
- `searchSchema` also defines the public filter names your API accepts. Those keys do not need to match database columns or even resource field names. Treat `searchSchema` as an allowlist plus normalization layer for filter input: the actual meaning of each key is still implemented by the backend through mappings such as `actualField`, multi-field rules such as `oneOf`, standard operators, or custom `applyFilter` logic. It is not a portable filter DSL.
310
-
311
- The merge rules and validation surface are shared across storage backends. Advanced execution semantics are still backend-owned. In practice, that means alias filters such as `oneOf` and custom `applyFilter` functions should be validated against the backend you actually run, and the SQL-heavy examples in this chapter assume the Knex-backed query layer.
312
-
313
- `searchSchema` also gives you the ability to define a search field that will search in multiple fields. For example:
314
-
315
- ```javascript
316
- searchSchema: {
317
- search: {
318
- type: 'string',
319
- oneOf: ['name', 'code'],
320
- filterOperator: 'like'
321
- }
322
- }
323
-
324
- const searchGe = await api.resources.countries.query({
325
- queryParams: {
326
- filters: {
327
- search: 'ge'
328
- }
329
- }
330
- });
331
- // HTTP: GET /api/countries?filter[search]=ge
332
- // Returns: {
333
- // data: [{ id: '3', name: 'Georgia', code: 'GE' }, { id: '6', name: 'Germany', code: 'DE' }]
334
- // }
335
-
336
- console.log('Search for "ge":', inspect(searchGe))
337
- ```
338
-
339
- Will return:
340
-
341
- ```
342
- Search for "ge": { data: [
343
- { id: '3', name: 'Georgia', code: 'GE' },
344
- { id: '6', name: 'Germany', code: 'DE' }
345
- ], meta: {...}, links: {...} }
346
- ```
347
-
348
- This common pattern will give you the ability to create "global" search fields that will look in multiple fields.
349
-
350
- ### Merging searchSchema with search:true Fields
351
-
352
- When you combine `searchSchema` with fields marked `search: true`, the library intelligently merges them:
353
-
354
- ```javascript
355
- await api.addResource('products', {
356
- schema: {
357
- name: { type: 'string', search: true }, // Will be added to searchSchema
358
- description: { type: 'string', search: true }, // Will be added to searchSchema
359
- price: { type: 'number', search: true }, // Overridden by searchSchema below
360
- category_id: { type: 'number' }, // Not searchable
361
- sku: { type: 'string', search: true } // Will be added to searchSchema
362
- },
363
- searchSchema: {
364
- // Explicit searchSchema entries take precedence
365
- price: {
366
- type: 'number',
367
- filterOperator: 'between' // Overrides the search:true with custom operator
368
- },
369
- category_name: { // Virtual field (doesn't exist in schema)
370
- type: 'string',
371
- actualField: 'category.name',
372
- filterOperator: 'like'
373
- }
374
- }
375
- });
376
-
377
- // Result: The effective searchSchema includes:
378
- // - name (from search:true, uses default '=' operator)
379
- // - description (from search:true, uses default '=' operator)
380
- // - sku (from search:true, uses default '=' operator)
381
- // - price (from explicit searchSchema, uses 'between' operator)
382
- // - category_name (from explicit searchSchema, virtual field)
383
-
384
- // You can now search using all these fields:
385
- const results = await api.resources.products.query({
386
- queryParams: {
387
- filters: {
388
- name: 'Widget', // From search:true
389
- price: [10, 100], // From searchSchema with 'between'
390
- category_name: 'Electronics' // Virtual field from searchSchema
391
- }
392
- }
393
- });
394
- ```
395
-
396
- This merge behavior gives you the best of both worlds:
397
- - Quick setup with `search: true` for simple fields
398
- - Precise control with `searchSchema` for complex requirements
399
- - No need to repeat fields in searchSchema that already have `search: true`
400
-
401
- ### Multi-word Search with AND Logic
402
-
403
- 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.
404
-
405
- ```javascript
406
- await api.addResource('countries', {
407
- schema: {
408
- name: { type: 'string', required: true },
409
- code: { type: 'string', unique: true }
410
- },
411
- searchSchema: {
412
- search: {
413
- type: 'string',
414
- oneOf: ['name', 'code'],
415
- filterOperator: 'like',
416
- splitBy: ' ', // Split search terms by space
417
- matchAll: true // Require ALL terms to match (AND logic)
418
- }
419
- }
420
- });
421
- await api.resources.countries.createKnexTable()
422
- ```
423
-
424
- With this configuration, searching becomes much more precise:
425
-
426
- ```javascript
427
- // Add some countries
428
- await api.resources.countries.post({ name: 'United States', code: 'US' });
429
- await api.resources.countries.post({ name: 'United Kingdom', code: 'UK' });
430
- await api.resources.countries.post({ name: 'United Arab Emirates', code: 'AE' });
431
- await api.resources.countries.post({ name: 'South Africa', code: 'ZA' });
432
-
433
- // Search for "united states" - both words must appear
434
- const results = await api.resources.countries.query({
435
- queryParams: {
436
- filters: {
437
- search: 'united states'
438
- }
439
- }
440
- });
441
- // HTTP: GET /api/countries?filter[search]=united%20states
442
- // Returns: {
443
- // data: [{ id: '1', name: 'United States', code: 'US' }]
444
- // }
445
-
446
- console.log('Found:', results);
447
- // Note: results contains { data, meta, links } - access results.data for the array
448
- // Note: results now contains { data, meta, links } - access results.data for the array
449
- // Returns: [{ id: '1', name: 'United States', code: 'US' }]
450
- // Does NOT return United Kingdom or United Arab Emirates
451
- ```
452
-
453
- **How it works:**
454
-
455
- 1. The search term "united states" is split by space into ["united", "states"]
456
- 2. With `matchAll: true`, the query requires BOTH terms to appear
457
- 3. Each term can appear in ANY of the fields listed in `oneOf`
458
- 4. The SQL generated looks like:
459
-
460
- ```sql
461
- WHERE (
462
- (countries.name LIKE '%united%' OR countries.code LIKE '%united%')
463
- AND
464
- (countries.name LIKE '%states%' OR countries.code LIKE '%states%')
465
- )
466
- ```
467
-
468
- **More examples:**
469
-
470
- ```javascript
471
- // Search for "south africa" - finds only South Africa
472
- const southAfrica = await api.resources.countries.query({
473
- queryParams: { filters: { search: 'south africa' } }
474
- });
475
- // HTTP: GET /api/countries?filter[search]=south%20africa
476
- // Returns: {
477
- // data: [{ id: '4', name: 'South Africa', code: 'ZA' }]
478
- // }
479
-
480
- // Search for "united arab" - finds only United Arab Emirates
481
- const uae = await api.resources.countries.query({
482
- queryParams: { filters: { search: 'united arab' } }
483
- });
484
- // HTTP: GET /api/countries?filter[search]=united%20arab
485
- // Returns: {
486
- // data: [{ id: '3', name: 'United Arab Emirates', code: 'AE' }]
487
- // }
488
-
489
- // Single word searches still work normally
490
- const allUnited = await api.resources.countries.query({
491
- queryParams: { filters: { search: 'united' } }
492
- });
493
- // HTTP: GET /api/countries?filter[search]=united
494
- // Returns: {
495
- // data: [
496
- // { id: '1', name: 'United States', code: 'US' },
497
- // { id: '2', name: 'United Kingdom', code: 'UK' },
498
- // { id: '3', name: 'United Arab Emirates', code: 'AE' }
499
- // ]
500
- // }
501
- ```
502
-
503
- **Alternative configurations:**
504
-
505
- You can also use different separators and OR logic:
506
-
507
- ```javascript
508
- searchSchema: {
509
- // Comma-separated OR search
510
- tags: {
511
- type: 'string',
512
- oneOf: ['tags', 'categories', 'keywords'],
513
- filterOperator: 'like',
514
- splitBy: ',', // Split by comma
515
- matchAll: false // OR logic (default) - match ANY term
516
- },
517
-
518
- // Exact match with AND logic
519
- codes: {
520
- type: 'string',
521
- oneOf: ['primary_code', 'secondary_code'],
522
- filterOperator: '=', // Exact match
523
- splitBy: ' ',
524
- matchAll: true // All codes must match exactly
525
- }
526
- }
527
- ```
528
-
529
- This feature is particularly useful for:
530
- - Full-text search functionality where users type multiple words
531
- - Tag or keyword searches where all terms must be present
532
- - Product searches matching multiple criteria
533
- - Finding records that match complex multi-word queries
534
-
535
- ### Custom Search Functions
536
-
537
- If you need even more complex searches, you can use `searchSchema` to define search fields with custom query logic:
538
-
539
- ```javascript
540
- await api.addResource('countries', {
541
- schema: {
542
- name: { type: 'string', required: true },
543
- code: { type: 'string', unique: true }
544
- },
545
- searchSchema: {
546
- // Standard fields
547
- name: { type: 'string', filterOperator: '=' },
548
- code: { type: 'string', filterOperator: '=' },
549
-
550
- // Custom search using a function
551
- nameOrCode: {
552
- type: 'string',
553
- applyFilter: function(query, filterValue) {
554
- // Custom SQL logic: case-insensitive search in name OR exact match on code
555
- query.where(function() {
556
- this.whereRaw('LOWER(name) LIKE LOWER(?)', [`%${filterValue}%`])
557
- .orWhereRaw('LOWER(code) = LOWER(?)', [filterValue]);
558
- });
559
- }
560
- }
561
- }
562
- });
563
- await api.resources.countries.createKnexTable()
564
-
565
- await api.resources.countries.post({ name: 'United States', code: 'US' });
566
- await api.resources.countries.post({ name: 'United Kingdom', code: 'UK' });
567
- await api.resources.countries.post({ name: 'United Arab Emirates', code: 'AE' });
568
- await api.resources.countries.post({ name: 'South Africa', code: 'ZA' });
569
- ```
570
-
571
- When `filterOperator` is a function instead of an operator string, it receives:
572
- - `query` - The Knex query builder instance
573
- - `filterValue` - The search value from the user
574
- - `fieldName` - The name of the search field (optional third parameter)
575
-
576
- This gives you complete control over the SQL generated for that search field.
577
-
578
- **Example usage:**
579
-
580
- ```javascript
581
- // This custom search will find both:
582
- // - Countries with "at" in the name (United States, United Kingdom, United Arab Emirates)
583
- // - Countries with code "at"
584
- const results = await api.resources.countries.query({
585
- queryParams: {
586
- filters: {
587
- nameOrCode: 'at'
588
- }
589
- }
590
- });
591
- // HTTP: GET /api/countries?filter[nameOrCode]=at
592
- // Returns: {
593
- // data: [
594
- // { id: '1', name: 'United States', code: 'US' },
595
- // { id: '2', name: 'United Kingdom', code: 'UK' },
596
- // { id: '3', name: 'United Arab Emirates', code: 'AE' },
597
- // { id: '5', name: 'Austria', code: 'AT' }
598
- // ]
599
- // }
600
- ```
601
-
602
- The result is:
603
-
604
- ```text
605
- Query results: [
606
- { id: '1', name: 'United States', code: 'US' },
607
- { id: '3', name: 'United Arab Emirates', code: 'AE' }
608
- ]
609
- ```
610
-
611
- The function approach is powerful for:
612
- - Case-insensitive searches
613
- - Complex conditions combining multiple fields
614
- - Database-specific functions
615
- - Custom business logic in searches
616
-
617
- Since `applyFilter` functions tend to be database-dependent, it's best to avoid using it unless necessary.
618
-
619
- ### Sparse Fieldsets
620
-
621
- The JSON:API specification includes a powerful feature called "sparse fieldsets" that allows you to request only specific fields from a resource. This is essential for optimizing API performance by reducing payload sizes and network traffic.
622
-
623
- **How Sparse Fieldsets Work:**
624
-
625
- By default, API responses include all fields defined in the schema. With sparse fieldsets, you can specify exactly which fields you want returned. The `id` field is always included automatically as it's required by JSON:API.
626
-
627
- Let's work with our countries table:
628
-
629
- ```javascript
630
- await api.addResource('countries', {
631
- schema: {
632
- name: { type: 'string', required: true, max: 100, search: true },
633
- code: { type: 'string', max: 2, unique: true, search: true }
634
- }
635
- });
636
- await api.resources.countries.createKnexTable()
637
-
638
- // Add some test data
639
- await api.resources.countries.post({ name: 'France', code: 'FR' });
640
- await api.resources.countries.post({ name: 'Germany', code: 'DE' });
641
- await api.resources.countries.post({ name: 'Italy', code: 'IT' });
642
- await api.resources.countries.post({ name: 'United Kingdom', code: 'UK' });
643
- await api.resources.countries.post({ name: 'United States', code: 'US' });
644
- ```
645
-
646
- **Sparse Fieldsets with `get()` - Single Record:**
647
-
648
- ```javascript
649
- // Fetch a single country with all fields (default behavior)
650
- const fullCountry = await api.resources.countries.get({ id: '1' });
651
- console.log('Full record:', inspect(fullCountry));
652
-
653
- // Fetch only the name field
654
- const nameOnly = await api.resources.countries.get({
655
- id: '1',
656
- queryParams: { fields: { countries: 'name' } }
657
- });
658
- console.log('Name only:', inspect(nameOnly));
659
-
660
- // Fetch only the code field
661
- const codeOnly = await api.resources.countries.get({
662
- id: '1',
663
- queryParams: { fields: { countries: 'code' } }
664
- });
665
- console.log('Code only:', inspect(codeOnly));
666
- ```
667
-
668
- The output will be:
669
-
670
- ```text
671
- Full record: { id: '1', name: 'France', code: 'FR' }
672
- Name only: { id: '1', name: 'France' }
673
- Code only: { id: '1', code: 'FR' }
674
- ```
675
-
676
- **Sparse Fieldsets with `query()` - Multiple Records:**
677
-
678
- Sparse fieldsets become even more valuable when fetching collections, as they can significantly reduce the response size:
679
-
680
- ```javascript
681
- // Query all countries starting with 'United' - full records
682
- const fullRecords = await api.resources.countries.query({
683
- queryParams: {
684
- filters: { name: 'United' }
685
- }
686
- });
687
- // HTTP: GET /api/countries?filter[name]=United
688
- // Returns: {
689
- // data: [
690
- // { id: '4', name: 'United Kingdom', code: 'UK' },
691
- // { id: '5', name: 'United States', code: 'US' }
692
- // ]
693
- // }
694
-
695
- console.log('Full records:', inspect(fullRecords));
696
- // Note: fullRecords contains { data, meta, links }
697
-
698
- // Query with only names returned
699
- const namesOnly = await api.resources.countries.query({
700
- queryParams: {
701
- filters: { name: 'United' },
702
- fields: { countries: 'name' }
703
- }
704
- });
705
- // HTTP: GET /api/countries?filter[name]=United&fields[countries]=name
706
- // Returns: {
707
- // data: [
708
- // { id: '4', name: 'United Kingdom' },
709
- // { id: '5', name: 'United States' }
710
- // ]
711
- // }
712
-
713
- console.log('Names only:', inspect(namesOnly));
714
- // Note: namesOnly contains { data, meta, links }
715
-
716
- // Query with only codes returned
717
- const codesOnly = await api.resources.countries.query({
718
- queryParams: {
719
- filters: { name: 'United' },
720
- fields: { countries: 'code' }
721
- }
722
- });
723
- // HTTP: GET /api/countries?filter[name]=United&fields[countries]=code
724
- // Returns: {
725
- // data: [
726
- // { id: '4', code: 'UK' },
727
- // { id: '5', code: 'US' }
728
- // ]
729
- // }
730
-
731
- console.log('Codes only:', inspect(codesOnly));
732
- // Note: codesOnly contains { data, meta, links }
733
- ```
734
-
735
- **Combining Sparse Fieldsets with Complex Searches:**
736
-
737
- Sparse fieldsets work seamlessly with all search features, including our new multi-word search:
738
-
739
- ```javascript
740
- // Define a countries resource with multi-word search
741
- await api.addResource('countries', {
742
- schema: {
743
- name: { type: 'string', required: true },
744
- code: { type: 'string', unique: true },
745
- population: { type: 'integer' },
746
- continent: { type: 'string' }
747
- },
748
- searchSchema: {
749
- search: {
750
- type: 'string',
751
- oneOf: ['name', 'continent'],
752
- filterOperator: 'like',
753
- splitBy: ' ',
754
- matchAll: true
755
- }
756
- }
757
- });
758
-
759
- // Add countries with more fields
760
- await api.resources.countries.post({
761
- name: 'South Africa',
762
- code: 'ZA',
763
- population: 59308690,
764
- continent: 'Africa'
765
- });
766
- await api.resources.countries.post({
767
- name: 'South Korea',
768
- code: 'KR',
769
- population: 51269185,
770
- continent: 'Asia'
771
- });
772
-
773
- // Search for "south africa" but return only name and population
774
- const sparseSearch = await api.resources.countries.query({
775
- queryParams: {
776
- filters: { search: 'south africa' },
777
- fields: { countries: 'name,population' }
778
- }
779
- });
780
- // HTTP: GET /api/countries?filter[search]=south%20africa&fields[countries]=name,population
781
- // Returns: {
782
- // data: [{ id: '1', name: 'South Africa', population: 59308690 }]
783
- // }
784
-
785
- console.log('Sparse search result:', inspect(sparseSearch));
786
- // Note: sparseSearch contains { data, meta, links }
787
- ```
788
-
789
- **Important Notes:**
790
-
791
- 1. **The `id` field is always included** - This is required by the JSON:API specification
792
- 2. **Field names must match schema** - Requesting non-existent fields will be ignored
793
- 3. **Improves performance** - Especially important for large records or when fetching many records
794
- 4. **Works with relationships** - When we cover relationships, you'll see how to apply sparse fieldsets to related resources too
795
-
796
- **HTTP API Usage:**
797
-
798
- When using the HTTP API, sparse fieldsets are specified as comma-separated values:
799
-
800
- ```
801
- GET /api/countries?fields[countries]=name,code
802
- GET /api/countries/1?fields[countries]=name
803
- GET /api/countries?filter[search]=united+states&fields[countries]=code
804
- ```
805
-
806
- ---
807
-
808
- [Previous: 2.1 The starting point](./GUIDE_2_1_The_Starting_Point.md) | [Back to Guide](./README.md) | [Next: 2.3 `belongsTo` Relationships](./GUIDE_2_3_BelongsTo_Relationships.md)