json-rest-api 1.0.6 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/.claude/agents/library-guide-expert.md +29 -0
  2. package/.claude/agents/rest-api-internals-expert.md +53 -0
  3. package/CLAUDE.md +139 -1
  4. package/README.md +2 -5
  5. package/docTest.js +238 -0
  6. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +955 -0
  7. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +54 -0
  8. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +750 -0
  9. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +494 -0
  10. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +398 -0
  11. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +882 -0
  12. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +709 -0
  13. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +1233 -0
  14. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +796 -0
  15. package/{README_X_Relationship_Plugin.md → docs/GUIDE/GUIDE_2_9_Relationships_Urls.md} +1 -4
  16. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +66 -0
  17. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +1246 -0
  18. package/docs/GUIDE/GUIDE_5_Non-Db_Stores.md +0 -0
  19. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1490 -0
  20. package/{README_X_SocketIO.md → docs/GUIDE/GUIDE_X_SocketIO.md} +17 -17
  21. package/docs/GUIDE/index.md +58 -0
  22. package/docs/GUIDE.md +61 -2238
  23. package/docs/ONBOARDING/Computed_Properties_And_Sparse_Fields.md +398 -0
  24. package/docs/README.md +57 -2239
  25. package/{README_2_Computed_and_hidden_fields.md → docs/README_VARIOUS/GUIDE_2_Computed_and_hidden_fields.md} +22 -17
  26. package/docs/README_VARIOUS/GUIDE_IN_DOCS.md +2277 -0
  27. package/{README.ORIG.md → docs/README_VARIOUS/README.ORIG.md} +8 -8
  28. package/{README_8_Polymorphic_Relationships.md → docs/README_VARIOUS/README_8_Polymorphic_Relationships.md} +1 -1
  29. package/docs/README_VARIOUS/README_IN_DOCS.ms +2277 -0
  30. package/{RELATIONSHIPS_ADVANCED.md → docs/README_VARIOUS/RELATIONSHIPS_ADVANCED.md} +3 -3
  31. package/{correct-schema-format.md → docs/README_VARIOUS/correct-schema-format.md} +13 -13
  32. package/{pagination.md → docs/README_VARIOUS/pagination.md} +11 -8
  33. package/docs/assets/css/style.scss +78 -5
  34. package/docs/index.md +2 -12
  35. package/helper-reorganization-plan.md +85 -0
  36. package/helper-usage-analysis.md +94 -0
  37. package/package.json +5 -4
  38. package/plugins/core/bulk-operations-plugin.js +2 -2
  39. package/plugins/core/connectors/express-plugin.js +99 -33
  40. package/plugins/core/lib/querying/knex-cross-table-search.js +609 -0
  41. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +491 -0
  42. package/plugins/core/lib/querying/knex-pagination-helpers.js +544 -0
  43. package/plugins/core/lib/{knex-process-includes.js → querying/knex-process-includes.js} +65 -50
  44. package/plugins/core/lib/querying/knex-query-helpers-base.js +74 -0
  45. package/plugins/core/lib/{knex-query-helpers.js → querying/knex-query-helpers.js} +416 -180
  46. package/plugins/core/lib/{knex-relationship-includes.js → querying/knex-relationship-includes.js} +430 -114
  47. package/plugins/core/lib/querying/knex-window-queries.js +328 -0
  48. package/plugins/core/lib/querying-writing/compile-schemas.js +328 -0
  49. package/plugins/core/{utils → lib/querying-writing}/connectors-query-parser.js +62 -57
  50. package/plugins/core/lib/{database-capabilities.js → querying-writing/database-capabilities.js} +113 -7
  51. package/plugins/core/lib/querying-writing/database-value-normalizers.js +369 -0
  52. package/plugins/core/lib/querying-writing/default-data-helpers.js +72 -0
  53. package/plugins/core/{utils → lib/querying-writing}/field-utils.js +41 -31
  54. package/plugins/core/{utils → lib/querying-writing}/knex-constants.js +1 -0
  55. package/plugins/core/lib/{knex-field-helpers.js → querying-writing/knex-field-helpers.js} +190 -57
  56. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +162 -0
  57. package/plugins/core/lib/{payload-validators.js → querying-writing/payload-validators.js} +297 -217
  58. package/plugins/core/lib/querying-writing/schema-helpers.js +497 -0
  59. package/plugins/core/lib/querying-writing/scope-validations.js +431 -0
  60. package/plugins/core/lib/{simplified-helpers.js → querying-writing/simplified-helpers.js} +216 -176
  61. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +110 -0
  62. package/plugins/core/lib/writing/many-to-many-manipulations.js +300 -0
  63. package/plugins/core/lib/writing/relationship-processor.js +252 -0
  64. package/plugins/core/rest-api-knex-plugin.js +183 -62
  65. package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +7 -0
  66. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +72 -0
  67. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +102 -0
  68. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +50 -0
  69. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +39 -0
  70. package/plugins/core/rest-api-plugin-methods/add-route.js +23 -0
  71. package/plugins/core/rest-api-plugin-methods/check-permissions.js +18 -0
  72. package/plugins/core/rest-api-plugin-methods/common.js +605 -0
  73. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +115 -0
  74. package/plugins/core/rest-api-plugin-methods/delete.js +109 -0
  75. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +186 -0
  76. package/plugins/core/rest-api-plugin-methods/get-related.js +295 -0
  77. package/plugins/core/rest-api-plugin-methods/get-relationship.js +62 -0
  78. package/plugins/core/rest-api-plugin-methods/get.js +187 -0
  79. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +56 -0
  80. package/plugins/core/rest-api-plugin-methods/patch.js +193 -0
  81. package/plugins/core/rest-api-plugin-methods/post-relationship.js +104 -0
  82. package/plugins/core/rest-api-plugin-methods/post.js +157 -0
  83. package/plugins/core/rest-api-plugin-methods/put.js +257 -0
  84. package/plugins/core/rest-api-plugin-methods/query.js +208 -0
  85. package/plugins/core/rest-api-plugin.js +108 -2498
  86. package/plugins/core/socketio-plugin.js +28 -5
  87. package/plugins/storage/local-storage.js +2 -2
  88. package/tests/computed-fields.test.js +483 -0
  89. package/tests/cors.test.js +4 -4
  90. package/tests/curl.test.js +5 -4
  91. package/tests/custom-idproperty-relationships.test.js +891 -0
  92. package/tests/field-getters.test.js +353 -0
  93. package/tests/field-setters.test.js +310 -0
  94. package/tests/fixtures/api-configs.js +800 -28
  95. package/tests/jwt-auth.test.js +0 -2
  96. package/tests/nested-includes.test.js +1 -1
  97. package/tests/pagination-cursor-multifield.test.js +514 -0
  98. package/tests/pagination-enhanced.test.js +12 -9
  99. package/tests/polymorphic-relationships.test.js +10 -8
  100. package/tests/polymorphic-simple.test.js +6 -4
  101. package/tests/positioning-advanced.test.js +12 -12
  102. package/tests/positioning-extended.test.js +25 -25
  103. package/tests/positioning-multifilter.test.js +24 -24
  104. package/tests/positioning.test.js +3 -3
  105. package/tests/query-limits.test.js +24 -8
  106. package/tests/relationship-endpoints.test.js +4 -5
  107. package/tests/relationships.test.js +0 -1
  108. package/tests/socketio.test.js +12 -1
  109. package/tests/virtual-fields.test.js +13 -13
  110. package/PLAN_FOR_WEBSOCKETS.md +0 -398
  111. package/README_1_Initial_Setup.md +0 -393
  112. package/README_X_Hooks.md +0 -647
  113. package/TODO_rest-api-plugin-extras.todo +0 -23
  114. package/connectors_ideas/express-plugin.js +0 -120
  115. package/connectors_ideas/http-plugin.js +0 -138
  116. package/docs/examples/express-plugin.js +0 -366
  117. package/example.js +0 -301
  118. package/plugins/core/HELPER_MODULES.md +0 -285
  119. package/plugins/core/lib/compile-schemas.js +0 -245
  120. package/plugins/core/lib/database-value-normalizers.js +0 -192
  121. package/plugins/core/lib/default-data-helpers.js +0 -100
  122. package/plugins/core/lib/knex-cross-table-search.js +0 -442
  123. package/plugins/core/lib/knex-json-api-helpers.js +0 -84
  124. package/plugins/core/lib/knex-json-api-transformers.js +0 -449
  125. package/plugins/core/lib/knex-pagination-helpers.js +0 -193
  126. package/plugins/core/lib/knex-window-queries.js +0 -142
  127. package/plugins/core/lib/many-to-many-manipulations.js +0 -343
  128. package/plugins/core/lib/relationship-processor.js +0 -278
  129. package/plugins/core/lib/schema-helpers.js +0 -343
  130. package/plugins/core/lib/scope-validations.js +0 -252
  131. package/plugins/core/rest-api-relationships-plugin.js +0 -700
  132. package/plugins/core/utils/knex-query-helpers-base.js +0 -56
  133. /package/{README_X_Permissions.md → docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md} +0 -0
  134. /package/{README_X_Positioning.md → docs/GUIDE/GUIDE_6_Positioning.md} +0 -0
  135. /package/{README_X_Bulk_Operations.md → docs/GUIDE/GUIDE_X_Bulk_Operations.md} +0 -0
  136. /package/{README_X_Cors.md → docs/GUIDE/GUIDE_X_Cors.md} +0 -0
  137. /package/{README_X_Multihome.md → docs/GUIDE/GUIDE_X_Multihome.md} +0 -0
  138. /package/{README_X_Appendices.md → docs/GUIDE/GUIDE_Y_Appendices.md} +0 -0
  139. /package/{QUERY_DEPTH_PROTECTION.md → docs/README_VARIOUS/QUERY_DEPTH_PROTECTION.md} +0 -0
  140. /package/{RELATIONSHIPS.md → docs/README_VARIOUS/RELATIONSHIPS.md} +0 -0
  141. /package/{WEBSOCKET_IMPLEMENTATION_STATUS.md → docs/README_VARIOUS/WEBSOCKET_IMPLEMENTATION_STATUS.md} +0 -0
  142. /package/{helper-functions-chart.md → docs/README_VARIOUS/helper-functions-chart.md} +0 -0
  143. /package/docs/{implementation-hidden-computed-fields.md → README_VARIOUS/implementation-hidden-computed-fields.md} +0 -0
  144. /package/docs/{include-limits-migration-guide.md → README_VARIOUS/include-limits-migration-guide.md} +0 -0
  145. /package/{readmeSnippets → docs/README_VARIOUS}/test_put_behavior.js +0 -0
  146. /package/{readmeSnippets → docs/README_VARIOUS}/test_put_fields_without_defaults.js +0 -0
@@ -0,0 +1,750 @@
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/1.0/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/1.0/countries/2"}},"links":{"self":"/api/1.0/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 completely replaces the search configuration from the main schema.** Only fields defined in the `searchSchema` are searchable if `searchSchema` is defined.
298
+
299
+ The `searchSchema` gives you:
300
+
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.
305
+
306
+ `searchSchema` also gives you the ability to define a search field that will search in multiple fields. For example:
307
+
308
+ ```javascript
309
+ searchSchema: {
310
+ search: {
311
+ type: 'string',
312
+ oneOf: ['name', 'code'],
313
+ filterOperator: 'like'
314
+ }
315
+ }
316
+
317
+ const searchGe = await api.resources.countries.query({
318
+ queryParams: {
319
+ filters: {
320
+ search: 'ge'
321
+ }
322
+ }
323
+ });
324
+ // HTTP: GET /api/countries?filter[search]=ge
325
+ // Returns: {
326
+ // data: [{ id: '3', name: 'Georgia', code: 'GE' }, { id: '6', name: 'Germany', code: 'DE' }]
327
+ // }
328
+
329
+ console.log('Search for "ge":', inspect(searchGe))
330
+ ```
331
+
332
+ Will return:
333
+
334
+ ```
335
+ Search for "ge": { data: [
336
+ { id: '3', name: 'Georgia', code: 'GE' },
337
+ { id: '6', name: 'Germany', code: 'DE' }
338
+ ], meta: {...}, links: {...} }
339
+ ```
340
+
341
+ This common pattern will give you the ability to create "global" search fields that will look in multiple fields.
342
+
343
+ ### Multi-word Search with AND Logic
344
+
345
+ 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.
346
+
347
+ ```javascript
348
+ await api.addResource('countries', {
349
+ schema: {
350
+ name: { type: 'string', required: true },
351
+ code: { type: 'string', unique: true }
352
+ },
353
+ searchSchema: {
354
+ search: {
355
+ type: 'string',
356
+ oneOf: ['name', 'code'],
357
+ filterOperator: 'like',
358
+ splitBy: ' ', // Split search terms by space
359
+ matchAll: true // Require ALL terms to match (AND logic)
360
+ }
361
+ }
362
+ });
363
+ await api.resources.countries.createKnexTable()
364
+ ```
365
+
366
+ With this configuration, searching becomes much more precise:
367
+
368
+ ```javascript
369
+ // Add some countries
370
+ await api.resources.countries.post({ name: 'United States', code: 'US' });
371
+ await api.resources.countries.post({ name: 'United Kingdom', code: 'UK' });
372
+ await api.resources.countries.post({ name: 'United Arab Emirates', code: 'AE' });
373
+ await api.resources.countries.post({ name: 'South Africa', code: 'ZA' });
374
+
375
+ // Search for "united states" - both words must appear
376
+ const results = await api.resources.countries.query({
377
+ queryParams: {
378
+ filters: {
379
+ search: 'united states'
380
+ }
381
+ }
382
+ });
383
+ // HTTP: GET /api/countries?filter[search]=united%20states
384
+ // Returns: {
385
+ // data: [{ id: '1', name: 'United States', code: 'US' }]
386
+ // }
387
+
388
+ console.log('Found:', results);
389
+ // Note: results contains { data, meta, links } - access results.data for the array
390
+ // Note: results now contains { data, meta, links } - access results.data for the array
391
+ // Returns: [{ id: '1', name: 'United States', code: 'US' }]
392
+ // Does NOT return United Kingdom or United Arab Emirates
393
+ ```
394
+
395
+ **How it works:**
396
+
397
+ 1. The search term "united states" is split by space into ["united", "states"]
398
+ 2. With `matchAll: true`, the query requires BOTH terms to appear
399
+ 3. Each term can appear in ANY of the fields listed in `oneOf`
400
+ 4. The SQL generated looks like:
401
+
402
+ ```sql
403
+ WHERE (
404
+ (countries.name LIKE '%united%' OR countries.code LIKE '%united%')
405
+ AND
406
+ (countries.name LIKE '%states%' OR countries.code LIKE '%states%')
407
+ )
408
+ ```
409
+
410
+ **More examples:**
411
+
412
+ ```javascript
413
+ // Search for "south africa" - finds only South Africa
414
+ const southAfrica = await api.resources.countries.query({
415
+ queryParams: { filters: { search: 'south africa' } }
416
+ });
417
+ // HTTP: GET /api/countries?filter[search]=south%20africa
418
+ // Returns: {
419
+ // data: [{ id: '4', name: 'South Africa', code: 'ZA' }]
420
+ // }
421
+
422
+ // Search for "united arab" - finds only United Arab Emirates
423
+ const uae = await api.resources.countries.query({
424
+ queryParams: { filters: { search: 'united arab' } }
425
+ });
426
+ // HTTP: GET /api/countries?filter[search]=united%20arab
427
+ // Returns: {
428
+ // data: [{ id: '3', name: 'United Arab Emirates', code: 'AE' }]
429
+ // }
430
+
431
+ // Single word searches still work normally
432
+ const allUnited = await api.resources.countries.query({
433
+ queryParams: { filters: { search: 'united' } }
434
+ });
435
+ // HTTP: GET /api/countries?filter[search]=united
436
+ // Returns: {
437
+ // data: [
438
+ // { id: '1', name: 'United States', code: 'US' },
439
+ // { id: '2', name: 'United Kingdom', code: 'UK' },
440
+ // { id: '3', name: 'United Arab Emirates', code: 'AE' }
441
+ // ]
442
+ // }
443
+ ```
444
+
445
+ **Alternative configurations:**
446
+
447
+ You can also use different separators and OR logic:
448
+
449
+ ```javascript
450
+ searchSchema: {
451
+ // Comma-separated OR search
452
+ tags: {
453
+ type: 'string',
454
+ oneOf: ['tags', 'categories', 'keywords'],
455
+ filterOperator: 'like',
456
+ splitBy: ',', // Split by comma
457
+ matchAll: false // OR logic (default) - match ANY term
458
+ },
459
+
460
+ // Exact match with AND logic
461
+ codes: {
462
+ type: 'string',
463
+ oneOf: ['primary_code', 'secondary_code'],
464
+ filterOperator: '=', // Exact match
465
+ splitBy: ' ',
466
+ matchAll: true // All codes must match exactly
467
+ }
468
+ }
469
+ ```
470
+
471
+ This feature is particularly useful for:
472
+ - Full-text search functionality where users type multiple words
473
+ - Tag or keyword searches where all terms must be present
474
+ - Product searches matching multiple criteria
475
+ - Finding records that match complex multi-word queries
476
+
477
+ ### Custom Search Functions
478
+
479
+ If you need even more complex searches, you can use `searchSchema` to define search fields with custom query logic:
480
+
481
+ ```javascript
482
+ await api.addResource('countries', {
483
+ schema: {
484
+ name: { type: 'string', required: true },
485
+ code: { type: 'string', unique: true }
486
+ },
487
+ searchSchema: {
488
+ // Standard fields
489
+ name: { type: 'string', filterOperator: '=' },
490
+ code: { type: 'string', filterOperator: '=' },
491
+
492
+ // Custom search using a function
493
+ nameOrCode: {
494
+ type: 'string',
495
+ applyFilter: function(query, filterValue) {
496
+ // Custom SQL logic: case-insensitive search in name OR exact match on code
497
+ query.where(function() {
498
+ this.whereRaw('LOWER(name) LIKE LOWER(?)', [`%${filterValue}%`])
499
+ .orWhereRaw('LOWER(code) = LOWER(?)', [filterValue]);
500
+ });
501
+ }
502
+ }
503
+ }
504
+ });
505
+ await api.resources.countries.createKnexTable()
506
+
507
+ await api.resources.countries.post({ name: 'United States', code: 'US' });
508
+ await api.resources.countries.post({ name: 'United Kingdom', code: 'UK' });
509
+ await api.resources.countries.post({ name: 'United Arab Emirates', code: 'AE' });
510
+ await api.resources.countries.post({ name: 'South Africa', code: 'ZA' });
511
+ ```
512
+
513
+ When `filterOperator` is a function instead of an operator string, it receives:
514
+ - `query` - The Knex query builder instance
515
+ - `filterValue` - The search value from the user
516
+ - `fieldName` - The name of the search field (optional third parameter)
517
+
518
+ This gives you complete control over the SQL generated for that search field.
519
+
520
+ **Example usage:**
521
+
522
+ ```javascript
523
+ // This custom search will find both:
524
+ // - Countries with "at" in the name (United States, United Kingdom, United Arab Emirates)
525
+ // - Countries with code "at"
526
+ const results = await api.resources.countries.query({
527
+ queryParams: {
528
+ filters: {
529
+ nameOrCode: 'at'
530
+ }
531
+ }
532
+ });
533
+ // HTTP: GET /api/countries?filter[nameOrCode]=at
534
+ // Returns: {
535
+ // data: [
536
+ // { id: '1', name: 'United States', code: 'US' },
537
+ // { id: '2', name: 'United Kingdom', code: 'UK' },
538
+ // { id: '3', name: 'United Arab Emirates', code: 'AE' },
539
+ // { id: '5', name: 'Austria', code: 'AT' }
540
+ // ]
541
+ // }
542
+ ```
543
+
544
+ The result is:
545
+
546
+ ```text
547
+ Query results: [
548
+ { id: '1', name: 'United States', code: 'US' },
549
+ { id: '3', name: 'United Arab Emirates', code: 'AE' }
550
+ ]
551
+ ```
552
+
553
+ The function approach is powerful for:
554
+ - Case-insensitive searches
555
+ - Complex conditions combining multiple fields
556
+ - Database-specific functions
557
+ - Custom business logic in searches
558
+
559
+ Since `applyFilter` functions tend to be database-dependent, it's best to avoid using it unless necessary.
560
+
561
+ ### Sparse Fieldsets
562
+
563
+ 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.
564
+
565
+ **How Sparse Fieldsets Work:**
566
+
567
+ 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.
568
+
569
+ Let's work with our countries table:
570
+
571
+ ```javascript
572
+ await api.addResource('countries', {
573
+ schema: {
574
+ name: { type: 'string', required: true, max: 100, search: true },
575
+ code: { type: 'string', max: 2, unique: true, search: true }
576
+ }
577
+ });
578
+ await api.resources.countries.createKnexTable()
579
+
580
+ // Add some test data
581
+ await api.resources.countries.post({ name: 'France', code: 'FR' });
582
+ await api.resources.countries.post({ name: 'Germany', code: 'DE' });
583
+ await api.resources.countries.post({ name: 'Italy', code: 'IT' });
584
+ await api.resources.countries.post({ name: 'United Kingdom', code: 'UK' });
585
+ await api.resources.countries.post({ name: 'United States', code: 'US' });
586
+ ```
587
+
588
+ **Sparse Fieldsets with `get()` - Single Record:**
589
+
590
+ ```javascript
591
+ // Fetch a single country with all fields (default behavior)
592
+ const fullCountry = await api.resources.countries.get({ id: '1' });
593
+ console.log('Full record:', inspect(fullCountry));
594
+
595
+ // Fetch only the name field
596
+ const nameOnly = await api.resources.countries.get({
597
+ id: '1',
598
+ queryParams: { fields: { countries: 'name' } }
599
+ });
600
+ console.log('Name only:', inspect(nameOnly));
601
+
602
+ // Fetch only the code field
603
+ const codeOnly = await api.resources.countries.get({
604
+ id: '1',
605
+ queryParams: { fields: { countries: 'code' } }
606
+ });
607
+ console.log('Code only:', inspect(codeOnly));
608
+ ```
609
+
610
+ The output will be:
611
+
612
+ ```text
613
+ Full record: { id: '1', name: 'France', code: 'FR' }
614
+ Name only: { id: '1', name: 'France' }
615
+ Code only: { id: '1', code: 'FR' }
616
+ ```
617
+
618
+ **Sparse Fieldsets with `query()` - Multiple Records:**
619
+
620
+ Sparse fieldsets become even more valuable when fetching collections, as they can significantly reduce the response size:
621
+
622
+ ```javascript
623
+ // Query all countries starting with 'United' - full records
624
+ const fullRecords = await api.resources.countries.query({
625
+ queryParams: {
626
+ filters: { name: 'United' }
627
+ }
628
+ });
629
+ // HTTP: GET /api/countries?filter[name]=United
630
+ // Returns: {
631
+ // data: [
632
+ // { id: '4', name: 'United Kingdom', code: 'UK' },
633
+ // { id: '5', name: 'United States', code: 'US' }
634
+ // ]
635
+ // }
636
+
637
+ console.log('Full records:', inspect(fullRecords));
638
+ // Note: fullRecords contains { data, meta, links }
639
+
640
+ // Query with only names returned
641
+ const namesOnly = await api.resources.countries.query({
642
+ queryParams: {
643
+ filters: { name: 'United' },
644
+ fields: { countries: 'name' }
645
+ }
646
+ });
647
+ // HTTP: GET /api/countries?filter[name]=United&fields[countries]=name
648
+ // Returns: {
649
+ // data: [
650
+ // { id: '4', name: 'United Kingdom' },
651
+ // { id: '5', name: 'United States' }
652
+ // ]
653
+ // }
654
+
655
+ console.log('Names only:', inspect(namesOnly));
656
+ // Note: namesOnly contains { data, meta, links }
657
+
658
+ // Query with only codes returned
659
+ const codesOnly = await api.resources.countries.query({
660
+ queryParams: {
661
+ filters: { name: 'United' },
662
+ fields: { countries: 'code' }
663
+ }
664
+ });
665
+ // HTTP: GET /api/countries?filter[name]=United&fields[countries]=code
666
+ // Returns: {
667
+ // data: [
668
+ // { id: '4', code: 'UK' },
669
+ // { id: '5', code: 'US' }
670
+ // ]
671
+ // }
672
+
673
+ console.log('Codes only:', inspect(codesOnly));
674
+ // Note: codesOnly contains { data, meta, links }
675
+ ```
676
+
677
+ **Combining Sparse Fieldsets with Complex Searches:**
678
+
679
+ Sparse fieldsets work seamlessly with all search features, including our new multi-word search:
680
+
681
+ ```javascript
682
+ // Define a countries resource with multi-word search
683
+ await api.addResource('countries', {
684
+ schema: {
685
+ name: { type: 'string', required: true },
686
+ code: { type: 'string', unique: true },
687
+ population: { type: 'integer' },
688
+ continent: { type: 'string' }
689
+ },
690
+ searchSchema: {
691
+ search: {
692
+ type: 'string',
693
+ oneOf: ['name', 'continent'],
694
+ filterOperator: 'like',
695
+ splitBy: ' ',
696
+ matchAll: true
697
+ }
698
+ }
699
+ });
700
+
701
+ // Add countries with more fields
702
+ await api.resources.countries.post({
703
+ name: 'South Africa',
704
+ code: 'ZA',
705
+ population: 59308690,
706
+ continent: 'Africa'
707
+ });
708
+ await api.resources.countries.post({
709
+ name: 'South Korea',
710
+ code: 'KR',
711
+ population: 51269185,
712
+ continent: 'Asia'
713
+ });
714
+
715
+ // Search for "south africa" but return only name and population
716
+ const sparseSearch = await api.resources.countries.query({
717
+ queryParams: {
718
+ filters: { search: 'south africa' },
719
+ fields: { countries: 'name,population' }
720
+ }
721
+ });
722
+ // HTTP: GET /api/countries?filter[search]=south%20africa&fields[countries]=name,population
723
+ // Returns: {
724
+ // data: [{ id: '1', name: 'South Africa', population: 59308690 }]
725
+ // }
726
+
727
+ console.log('Sparse search result:', inspect(sparseSearch));
728
+ // Note: sparseSearch contains { data, meta, links }
729
+ ```
730
+
731
+ **Important Notes:**
732
+
733
+ 1. **The `id` field is always included** - This is required by the JSON:API specification
734
+ 2. **Field names must match schema** - Requesting non-existent fields will be ignored
735
+ 3. **Improves performance** - Especially important for large records or when fetching many records
736
+ 4. **Works with relationships** - When we cover relationships, you'll see how to apply sparse fieldsets to related resources too
737
+
738
+ **HTTP API Usage:**
739
+
740
+ When using the HTTP API, sparse fieldsets are specified as comma-separated values:
741
+
742
+ ```
743
+ GET /api/countries?fields[countries]=name,code
744
+ GET /api/countries/1?fields[countries]=name
745
+ GET /api/countries?filter[search]=united+states&fields[countries]=code
746
+ ```
747
+
748
+ ---
749
+
750
+ [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)