json-rest-api 1.0.29 → 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 (292) 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 +744 -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} +74 -30
  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 -177
  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 +48 -317
  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 +134 -37
  91. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  92. package/plugins/core/lib/querying/sort-helpers.js +13 -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 +179 -194
  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 +100 -290
  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 +93 -38
  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 +128 -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 -2485
  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 -1077
  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 -395
  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 -148
  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 +36 -68
  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 +37 -81
  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 -1247
  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 -148
  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 -155
  251. package/tests/fixtures/api-configs.js +0 -2566
  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-boundaries.test.js +0 -154
  264. package/tests/pagination-contract.test.js +0 -122
  265. package/tests/pagination-cursor-multifield.test.js +0 -641
  266. package/tests/pagination-enhanced.test.js +0 -563
  267. package/tests/polymorphic-relationships.test.js +0 -347
  268. package/tests/polymorphic-simple.test.js +0 -148
  269. package/tests/positioning-advanced.test.js +0 -692
  270. package/tests/positioning-extended.test.js +0 -915
  271. package/tests/positioning-multifilter.test.js +0 -699
  272. package/tests/positioning.test.js +0 -560
  273. package/tests/post-commit-hooks.test.js +0 -120
  274. package/tests/projected-fields.test.js +0 -319
  275. package/tests/public-exports.test.js +0 -15
  276. package/tests/put-create-parity.test.js +0 -216
  277. package/tests/queries.test.js +0 -521
  278. package/tests/query-limits.test.js +0 -584
  279. package/tests/relationship-endpoints.test.js +0 -620
  280. package/tests/relationship-include-storage-adapter.test.js +0 -56
  281. package/tests/relationships.test.js +0 -681
  282. package/tests/rest-api.test.js +0 -337
  283. package/tests/return-record-settings.test.js +0 -184
  284. package/tests/row-policy.test.js +0 -489
  285. package/tests/s3-storage.test.js +0 -38
  286. package/tests/searchschema-knex-behavior.test.js +0 -187
  287. package/tests/searchschema-merge.test.js +0 -151
  288. package/tests/socketio.test.js +0 -860
  289. package/tests/storage-mapping.test.js +0 -362
  290. package/tests/temporal-boundaries.test.js +0 -208
  291. package/tests/test-development-guide.md +0 -370
  292. package/tests/virtual-fields.test.js +0 -404
@@ -0,0 +1,266 @@
1
+ ---
2
+ title: "Query projections"
3
+ chapter: 15
4
+ chapter_label: "15"
5
+ ---
6
+
7
+ # 15. Query projections
8
+
9
+ Query projections are SQL-backed, output-only query fields provided by the optional `QueryProjectionsPlugin`. They are different from normal computed fields.
10
+
11
+ If you want to build your own plugin on the same query-field seam, see [Writing Plugins](29-writing-plugins.md).
12
+
13
+ - **Computed fields** run after the row is fetched, in JavaScript.
14
+ - **Projected fields** are selected inside the query, so they can participate in `SELECT`, `ORDER BY`, and cursor pagination.
15
+
16
+ Use query projection fields when a derived value must behave like a real list field.
17
+
18
+ Typical cases:
19
+ - sorting by a derived label such as `full_name`
20
+ - cursor pagination over a derived value
21
+ - returning a derived SQL value without storing it
22
+
23
+ Do **not** use query projection fields for normal response decoration. Regular computed fields are simpler and remain the right default for that.
24
+
25
+ ## Setup
26
+
27
+ The six executable blocks below form one scenario. Add them to the
28
+ [starting script](03-running-example.md) after REST/storage installation,
29
+ before starting the server, on a fresh database. Other code blocks are
30
+ configuration fragments.
31
+
32
+ ```javascript
33
+ import { QueryProjectionsPlugin } from 'json-rest-api'
34
+ await api.use(QueryProjectionsPlugin)
35
+ ```
36
+
37
+ ## Defining a query field
38
+
39
+ ```javascript
40
+ await api.addResource('authors', {
41
+ schema: {
42
+ id: { type: 'id' },
43
+ first_name: { type: 'string', required: true },
44
+ last_name: { type: 'string', required: true }
45
+ },
46
+
47
+ queryFields: {
48
+ full_name: {
49
+ type: 'string',
50
+ sortable: true,
51
+ select: ({ knex, column }) => knex.raw(
52
+ knex.client.config.client === 'mysql2'
53
+ ? "trim(concat(coalesce(??, ''), ' ', coalesce(??, '')))"
54
+ : "trim(coalesce(??, '') || ' ' || coalesce(??, ''))",
55
+ [column('first_name'), column('last_name')]
56
+ )
57
+ }
58
+ },
59
+
60
+ sortableFields: ['id', 'first_name', 'last_name'],
61
+ defaultSort: ['full_name']
62
+ })
63
+ await api.addResource('books', {
64
+ schema: {
65
+ title: { type: 'string', required: true },
66
+ author_id: { type: 'id', belongsTo: 'authors', as: 'author', nullable: true }
67
+ }
68
+ })
69
+ await api.resources.authors.createKnexTable()
70
+ await api.resources.books.createKnexTable()
71
+ ```
72
+
73
+ The expression uses `concat` for MySQL and `||` for SQLite/PostgreSQL. `column`
74
+ resolves logical fields to the selected storage representation.
75
+
76
+ ```javascript
77
+ const jane = await api.resources.authors.post({ format: 'plain', data: { first_name: 'Jane', last_name: 'Doe' } })
78
+ await api.resources.authors.post({ format: 'plain', data: { first_name: 'John', last_name: 'Adams' } })
79
+ await api.resources.authors.post({ format: 'plain', data: { first_name: 'Jane', last_name: 'Smith' } })
80
+ const book = await api.resources.books.post({ format: 'plain', data: { title: 'Projection example', author: jane.id } })
81
+ ```
82
+
83
+ `select()` is called at query time. It can return:
84
+
85
+ - a `knex.raw(...)` expression
86
+ - a Knex reference
87
+ - a Knex query/subquery expression
88
+
89
+ Return the expression directly from a synchronous callback. Knex raw values,
90
+ references and query builders are thenable: returning them from an `async`
91
+ callback or awaiting them can execute SQL instead of supplying an expression.
92
+ For example, use `select: ({ knex }) => knex.raw('?', [1])`, without `async`.
93
+
94
+ Definitions are compiled into `scope.vars.schemaInfo.queryFields` during schema
95
+ enrichment. Projection names cannot collide with stored/computed fields,
96
+ relationship names or logical ID names. Canonical field additions repeat these
97
+ checks before publishing a replacement schema. SQL expressions remain local to
98
+ each query; compiled definitions do not cache request-dependent SQL.
99
+
100
+ Getters and computed fields can declare projection names as dependencies. The
101
+ projection is selected before JavaScript callbacks run, including through a
102
+ chain of dependencies in a sparse fieldset. A projection needed only as an input
103
+ is removed from output unless selected and visible. Install the declaration
104
+ plugin before resource registration so dependencies can be validated.
105
+
106
+ The callback receives:
107
+
108
+ ```js
109
+ {
110
+ knex, // current knex/db handle
111
+ db, // same as knex
112
+ context, // request context
113
+ scopeName,
114
+ tableName,
115
+ fieldName,
116
+ schemaInfo,
117
+ adapter, // storage adapter
118
+ column, // helper: logical field -> qualified storage column
119
+ ref // helper: logical field -> knex ref
120
+ }
121
+ ```
122
+
123
+ ## Behavior
124
+
125
+ Query projection fields are:
126
+
127
+ - returned by default in `get()` and `query()`
128
+ - available in sparse fieldsets
129
+ - usable in sorting and cursor pagination
130
+ - available on included resources
131
+ - ignored on writes
132
+
133
+ They are **not** stored columns, and they are **not** part of write validation.
134
+
135
+ A failed `select()` callback or rejected promise rejects the operation. On reads,
136
+ typed API errors retain their identity; unexpected errors are wrapped with the
137
+ resource/field context and the original thrown value as `cause`, including
138
+ non-Error values. Included projection failures also reject GET/query instead of
139
+ returning a partial response. During a full write response, the failure occurs
140
+ before an owned transaction commits; the public `RestApiWriteError` retains the
141
+ failure as its cause and reports the transaction outcome. A borrowed transaction
142
+ remains its owner's responsibility.
143
+
144
+ ## Sparse fieldsets
145
+
146
+ Query projection fields can be requested explicitly:
147
+
148
+ ```javascript
149
+ const sparse = await api.resources.authors.get({
150
+ id: jane.id, format: 'jsonapi',
151
+ queryParams: {
152
+ fields: {
153
+ authors: 'first_name,full_name'
154
+ }
155
+ }
156
+ })
157
+ ```
158
+
159
+ If a query field is needed only for sorting, it is still selected internally, but it is removed from the response unless the sparse fieldset asked for it.
160
+
161
+ ## Sorting and cursor pagination
162
+
163
+ Query projection fields are useful when a derived value must drive list behavior:
164
+
165
+ ```javascript
166
+ const firstPage = await api.resources.authors.query({
167
+ format: 'jsonapi',
168
+ queryParams: { sort: ['full_name'], page: { size: 2 } }
169
+ })
170
+ const nextPage = await api.resources.authors.query({
171
+ format: 'jsonapi',
172
+ queryParams: { sort: ['full_name'], page: { size: 2, after: firstPage.meta.pagination.cursor.next } }
173
+ })
174
+ ```
175
+
176
+ `json-rest-api` automatically appends `id` as a stable tie-breaker for cursor ordering. That prevents duplicate or skipped rows when multiple records share the same projected value.
177
+
178
+ Projection cursors retain the SQL expression's value before output formatting
179
+ and bind it directly on the next request. This also preserves precise timestamps
180
+ beyond the built-in millisecond write limit:
181
+
182
+ ```js
183
+ queryFields: {
184
+ preciseTimestamp: {
185
+ type: 'dateTime',
186
+ temporalPrecision: 6,
187
+ sortable: true,
188
+ select: ({ knex, column }) => knex.raw('??', [column('recordedAt')])
189
+ }
190
+ }
191
+ ```
192
+
193
+ Projections cannot declare `storage`, `setter`, or `runSetterAfter`; compilation
194
+ rejects those options. Use the stored field's serializer when writing that field.
195
+ For projection output formatting, use `getter` and optional `runGetterAfter`.
196
+ The shared dependency order awaits projection getters before dependent getters
197
+ or computed fields, and sparse reads fetch their declared dependencies. Hidden
198
+ dependencies remain hidden in the response. Getter failures propagate as read
199
+ errors. Neither getters nor response formatting change SQL ordering or cursors.
200
+ Always follow returned cursors/links without rebuilding them from attributes.
201
+ See [temporal migration requirements](33-migrating-to-v2.md#temporal-formatting-and-native-databases)
202
+ for UTC bindings, fractional precision and existing AnyAPI data.
203
+
204
+ Structured (`object`/`array`) projections are output fields and cannot be marked
205
+ sortable. Select a scalar JSON key in a separate typed projection when it should
206
+ drive ordering or cursors. PostgreSQL structured selections use `to_jsonb` to
207
+ support `DISTINCT` parent queries; this does not alter application tables.
208
+ See [querying structured attributes](33-migrating-to-v2.md#querying-structured-attributes)
209
+ for bound SQL examples and custom-filter migration.
210
+
211
+ Hidden projections cannot be sortable or appear in resource/include sort
212
+ configuration: cursors contain the selected SQL value. Use `normallyHidden`
213
+ for a publicly readable projection that should be omitted by default but remain
214
+ sortable. A `hidden` projection can still supply a getter/computed dependency.
215
+ See [hidden sort migration](33-migrating-to-v2.md#hidden-sort-fields).
216
+
217
+ ## Included resources
218
+
219
+ Without a target fieldset, includes select all visible projections and the
220
+ dependencies of getters and computed fields. This applies to to-one, to-many
221
+ and polymorphic includes in both storage modes, even before the target's first
222
+ direct read. A sparse fieldset returns only the requested fields; dependencies
223
+ selected solely for a callback are removed from the response.
224
+
225
+ Query projection fields can be selected for included resources too:
226
+
227
+ ```javascript
228
+ const included = await api.resources.books.get({
229
+ id: book.id, format: 'jsonapi',
230
+ queryParams: {
231
+ include: ['author'],
232
+ fields: {
233
+ books: 'title,author',
234
+ authors: 'full_name'
235
+ }
236
+ }
237
+ })
238
+ ```
239
+
240
+ ## Write behavior
241
+
242
+ Query projection fields are output-only. If a client sends them in `POST`, `PUT`, or `PATCH`, they are ignored.
243
+
244
+ The configured logger receives a bounded warning with the operation, resource,
245
+ validation phase and total ignored-field count. It does not include submitted
246
+ attribute values. A failure to write this advisory warning does not reject the
247
+ operation.
248
+
249
+ That keeps the boundary clean:
250
+
251
+ - persisted fields own storage
252
+ - computed fields own response-layer enrichment
253
+ - query projection fields own query-layer derivation
254
+
255
+ ## Limits
256
+
257
+ Projected fields are an advanced query feature. They are intentionally **backend-sensitive**.
258
+
259
+ Important limits:
260
+
261
+ - projection SQL can be dialect-specific
262
+ - this is not a portability layer across every SQL engine
263
+ - query projection fields are not auto-searchable
264
+ - if you want to sort by one, mark it `sortable: true`
265
+
266
+ For display-only derived values, prefer normal computed fields from [Field Transformations](12-field-transformations.md).
@@ -1,4 +1,10 @@
1
- # Autofiltering
1
+ ---
2
+ title: "Autofiltering"
3
+ chapter: 16
4
+ chapter_label: "16"
5
+ ---
6
+
7
+ # 16. Autofiltering
2
8
 
3
9
  The `AutoFilterPlugin` provides declarative dataset scoping for `json-rest-api`.
4
10
 
@@ -31,17 +37,75 @@ The plugin is generic. It does not assume:
31
37
 
32
38
  Your application decides where scope values come from. The plugin only consumes configured resolver functions.
33
39
 
40
+ ## Runnable workspace example
41
+
42
+ Insert these three blocks into the [starting script](03-running-example.md)
43
+ after installing storage and before starting the server, on a fresh database.
44
+ They also work with canonical storage after its normal initialization. The
45
+ remaining examples below are configuration alternatives and fragments.
46
+
47
+ ```javascript
48
+ import { AutoFilterPlugin } from 'json-rest-api'
49
+
50
+ await api.use(AutoFilterPlugin, {
51
+ resolvers: { workspace: ({ context }) => context.session?.workspaceId },
52
+ presets: { workspace: { filters: [{ field: 'workspace_id', resolver: 'workspace' }] } }
53
+ })
54
+ await api.addResource('projects', {
55
+ schema: {
56
+ name: { type: 'string', required: true },
57
+ workspace_id: { type: 'string', required: true }
58
+ },
59
+ autofilter: 'workspace'
60
+ })
61
+ await api.resources.projects.createKnexTable()
62
+ const workspaceContext = workspaceId => ({ session: { workspaceId } })
63
+ ```
64
+
65
+ Supply trusted application context as the second argument. Here the workspace
66
+ values are illustrative; a server should derive them from its authenticated
67
+ request context.
68
+
69
+ ```javascript
70
+ const acmeProject = await api.resources.projects.post({
71
+ data: { name: 'Roadmap' }, format: 'plain'
72
+ }, workspaceContext('acme'))
73
+ await api.resources.projects.post({
74
+ data: { name: 'Other workspace' }, format: 'plain'
75
+ }, workspaceContext('other'))
76
+ const acmePage = await api.resources.projects.query({
77
+ format: 'jsonapi', queryParams: { page: { number: 1, size: 10 } }
78
+ }, workspaceContext('acme'))
79
+ console.log(acmeProject.workspace_id, acmePage.data, acmePage.meta.pagination.total)
80
+ ```
81
+
82
+ The created record has `workspace_id: 'acme'`. The page contains only Roadmap
83
+ and its total is 1, even though another workspace has a stored project.
84
+
85
+ ```javascript
86
+ const replacedProject = await api.resources.projects.put({
87
+ id: acmeProject.id, data: { name: 'Updated roadmap' }, format: 'plain'
88
+ }, workspaceContext('acme'))
89
+ const otherPage = await api.resources.projects.query({ format: 'plain' }, workspaceContext('other'))
90
+ console.log(replacedProject.workspace_id, otherPage.data.map(project => project.name))
91
+ ```
92
+
93
+ PUT supplies the complete writable record, with the scoped workspace field
94
+ injected by the plugin. The record remains in acme, and the other workspace
95
+ still sees only its own project. Explicitly submitting a different workspace
96
+ value rejects; omitting required application scope context also rejects.
97
+
34
98
  ## Installation
35
99
 
36
100
  ```js
37
- import { Api } from 'hooked-api'
101
+ import { JsonRestApi } from 'json-rest-api'
38
102
  import {
39
103
  RestApiPlugin,
40
104
  RestApiKnexPlugin,
41
105
  AutoFilterPlugin
42
106
  } from 'json-rest-api'
43
107
 
44
- const api = new Api({ name: 'scoped-api' })
108
+ const api = new JsonRestApi({ name: 'scoped-api' })
45
109
 
46
110
  await api.use(RestApiPlugin)
47
111
  await api.use(RestApiKnexPlugin, { knex })
@@ -84,7 +148,7 @@ await api.use(AutoFilterPlugin, {
84
148
  - `RestApiKnexPlugin` for resources backed by normal database tables
85
149
  - `RestApiAnyapiKnexPlugin` for logical resources backed by AnyAPI canonical storage
86
150
 
87
- An `Api` instance has one storage engine. Install either storage plugin, then install `AutoFilterPlugin`. If an application keeps core resources in normal tables and user-defined resources in AnyAPI, create one `Api` instance for each storage engine. Each instance owns its resources and autofilter configuration.
151
+ A `JsonRestApi` instance has one storage engine. Install either storage plugin, then install `AutoFilterPlugin`. If an application keeps core resources in normal tables and user-defined resources in AnyAPI, create one `JsonRestApi` instance for each storage engine. Each instance owns its resources and autofilter configuration.
88
152
 
89
153
  Autofilter definitions always use logical schema field names. The selected storage plugin translates those fields to normal table columns or AnyAPI canonical slots.
90
154
 
@@ -93,14 +157,14 @@ Autofilter definitions always use logical schema field names. The selected stora
93
157
  AnyAPI's `tenantId` is fixed when `RestApiAnyapiKnexPlugin` is installed. It identifies the metadata and canonical-storage namespace; it is not a per-request workspace value. For one AnyAPI instance serving multiple application workspaces, use a stable internal `tenantId` and persist `workspace_id` on every workspace-scoped resource:
94
158
 
95
159
  ```js
96
- import { Api } from 'hooked-api'
160
+ import { JsonRestApi } from 'json-rest-api'
97
161
  import {
98
162
  RestApiPlugin,
99
163
  RestApiAnyapiKnexPlugin,
100
164
  AutoFilterPlugin
101
165
  } from 'json-rest-api'
102
166
 
103
- const api = new Api({ name: 'form-data-api' })
167
+ const api = new JsonRestApi({ name: 'form-data-api' })
104
168
 
105
169
  await api.use(RestApiPlugin)
106
170
  await api.use(RestApiAnyapiKnexPlugin, {
@@ -222,7 +286,7 @@ For a `workspace_user` resource:
222
286
 
223
287
  ```js
224
288
  await api.resources.projects.query(
225
- { simplified: false },
289
+ { format: 'jsonapi' },
226
290
  {
227
291
  session: { workspaceId: 'acme' },
228
292
  subject: { id: 101 }
@@ -252,7 +316,7 @@ Example:
252
316
 
253
317
  ```js
254
318
  await api.resources.projects.post({
255
- inputRecord: {
319
+ document: {
256
320
  data: {
257
321
  type: 'projects',
258
322
  attributes: {
@@ -260,7 +324,7 @@ await api.resources.projects.post({
260
324
  }
261
325
  }
262
326
  },
263
- simplified: false
327
+ format: 'jsonapi'
264
328
  }, {
265
329
  session: { workspaceId: 'acme' },
266
330
  subject: { id: 101 }
@@ -297,7 +361,7 @@ So this will fail if the referenced project is outside scope:
297
361
 
298
362
  ```js
299
363
  await api.resources.tasks.post({
300
- inputRecord: {
364
+ document: {
301
365
  data: {
302
366
  type: 'tasks',
303
367
  attributes: { title: 'Cross-scope task' },
@@ -308,12 +372,10 @@ await api.resources.tasks.post({
308
372
  }
309
373
  }
310
374
  },
311
- simplified: false
375
+ format: 'jsonapi'
312
376
  }, {
313
- scopeValues: {
314
- workspaceId: 'workspace-a',
315
- userId: 101
316
- }
377
+ session: { workspaceId: 'workspace-a' },
378
+ subject: { id: 101 }
317
379
  })
318
380
  ```
319
381
 
@@ -1,4 +1,10 @@
1
- # Row Policies
1
+ ---
2
+ title: "Row policies"
3
+ chapter: 17
4
+ chapter_label: "17"
5
+ ---
6
+
7
+ # 17. Row policies
2
8
 
3
9
  `RowPolicyPlugin` is the server-side visibility seam for resources whose readable rows depend on more than a fixed equality filter.
4
10
 
@@ -18,7 +24,7 @@ This is unsafe and paginates the wrong dataset:
18
24
  ```js
19
25
  const page = await api.resources.organisation_units.query({
20
26
  queryParams: { page: { number: 1, size: 20 } },
21
- simplified: false
27
+ format: 'jsonapi'
22
28
  }, context)
23
29
 
24
30
  page.data = page.data.filter((unit) => canSeeUnit(context, unit))
@@ -66,7 +72,7 @@ The page size and count now describe the visible dataset.
66
72
  | Requirement | Use |
67
73
  | --- | --- |
68
74
  | The caller asks to filter by a public search field | Normal `queryParams.filters` / `searchSchema` |
69
- | Every row must match one or more context-derived persisted fields, and those fields should be stamped on writes | [`AutoFilterPlugin`](GUIDE_X_Autofiltering.md) |
75
+ | Every row must match one or more context-derived persisted fields, and those fields should be stamped on writes | [`AutoFilterPlugin`](16-autofiltering.md) |
70
76
  | Visibility needs `OR`, `EXISTS`, joins, hierarchy traversal, or domain-composed SQL | `RowPolicyPlugin` |
71
77
  | The caller may perform an action such as update, approve, or delete | Permission hooks, usually in addition to a row policy |
72
78
  | A response-only field must be calculated | Computed fields or query projections |
@@ -77,46 +83,14 @@ The page size and count now describe the visible dataset.
77
83
 
78
84
  Install the REST plugin, one supported Knex storage plugin, and then the row-policy plugin. Install `RowPolicyPlugin` before adding resources that declare `rowPolicy`.
79
85
 
80
- ### Normal table storage
81
-
82
- ```js
83
- import { Api } from 'hooked-api'
84
- import {
85
- RestApiPlugin,
86
- RestApiKnexPlugin,
87
- RowPolicyPlugin
88
- } from 'json-rest-api'
89
-
90
- const api = new Api({ name: 'application-api' })
91
-
92
- await api.use(RestApiPlugin)
93
- await api.use(RestApiKnexPlugin, { knex })
94
- await api.use(RowPolicyPlugin, {
95
- policies: {
96
- workspaceMember: ({ query, context, column, value }) => {
97
- const workspaceId = context.session?.workspaceId
98
- if (!workspaceId) return false
99
-
100
- query.where(
101
- column('workspace_id'),
102
- value('workspace_id', workspaceId)
103
- )
104
- return true
105
- }
106
- }
107
- })
108
- ```
109
-
110
- ### AnyAPI canonical storage
86
+ The following example starts with an `api` that already has `RestApiPlugin` and
87
+ one of `RestApiKnexPlugin` or `RestApiAnyapiKnexPlugin` installed, as described in
88
+ [initial setup](01-getting-started.md). Use a fresh database for this example.
89
+ The policy code is the same for both storage modes:
111
90
 
112
- Use the same plugin and logical field names:
91
+ ```javascript
92
+ import { RowPolicyPlugin } from 'json-rest-api'
113
93
 
114
- ```js
115
- await api.use(RestApiPlugin)
116
- await api.use(RestApiAnyapiKnexPlugin, {
117
- knex,
118
- tenantId: 'application'
119
- })
120
94
  await api.use(RowPolicyPlugin, {
121
95
  policies: {
122
96
  workspaceMember: ({ query, context, column, value }) => {
@@ -133,7 +107,8 @@ await api.use(RowPolicyPlugin, {
133
107
  })
134
108
  ```
135
109
 
136
- `column()` and `value()` translate logical schema fields and values through the active storage adapter. The example therefore works with both normal tables and AnyAPI canonical slots.
110
+ `column()` and `value()` translate logical schema fields and values through the
111
+ active storage adapter, including canonical slots.
137
112
 
138
113
  ## Resource configuration
139
114
 
@@ -141,7 +116,7 @@ await api.use(RowPolicyPlugin, {
141
116
 
142
117
  Registered policies are preferred for shared or package-owned behavior:
143
118
 
144
- ```js
119
+ ```javascript
145
120
  await api.addResource('documents', {
146
121
  schema: {
147
122
  id: { type: 'id' },
@@ -151,10 +126,44 @@ await api.addResource('documents', {
151
126
  rowPolicy: 'workspaceMember',
152
127
  tableName: 'documents'
153
128
  })
129
+ await api.resources.documents.createKnexTable()
154
130
  ```
155
131
 
156
132
  The string must match a function in `RowPolicyPlugin`'s `policies` option. Unknown names fail while the resource is being added.
157
133
 
134
+ Create two documents in one workspace and one in another. Supply trusted
135
+ application context separately from the call parameters:
136
+
137
+ ```javascript
138
+ const acmeContext = { session: { workspaceId: 'acme' } }
139
+ const otherContext = { session: { workspaceId: 'other' } }
140
+ for (const title of ['Alpha', 'Beta']) {
141
+ await api.resources.documents.post({
142
+ data: { title, workspace_id: 'acme' }, returning: 'none'
143
+ }, acmeContext)
144
+ }
145
+ await api.resources.documents.post({
146
+ data: { title: 'Other workspace', workspace_id: 'other' }, returning: 'none'
147
+ }, otherContext)
148
+
149
+ const acmePage = await api.resources.documents.query({
150
+ format: 'jsonapi',
151
+ queryParams: { sort: 'title', page: { number: 1, size: 1 } }
152
+ }, acmeContext)
153
+ const otherPage = await api.resources.documents.query({}, otherContext)
154
+ const deniedPage = await api.resources.documents.query({})
155
+
156
+ console.log(acmePage.data[0].attributes.title) // Alpha
157
+ console.log(acmePage.meta.pagination.total) // 2, before the page limit
158
+ console.log(otherPage.data.map(document => document.title)) // ['Other workspace']
159
+ console.log(deniedPage.data) // []: no workspace in the caller context
160
+ ```
161
+
162
+ These inserts explicitly supply `workspace_id`. This policy does not stamp or
163
+ validate ownership on writes; use autofilter and write permissions for that
164
+ contract. The example demonstrates read visibility, counts and missing-context
165
+ denial.
166
+
158
167
  ### Inline policy
159
168
 
160
169
  An inline function is supported for a policy that belongs to one resource:
@@ -279,8 +288,9 @@ Policies are reused for the resource selections that define externally visible d
279
288
  | `count` | A separate count query, including native offset-pagination counts and standalone count helpers. |
280
289
  | `single` | Minimal lookup used by `get`, and by existing-record preflights for `put`, `patch`, and `delete`. |
281
290
  | `include` | A target resource loaded into JSON:API `included` data. |
291
+ | `search-join` | A related resource used by a cross-resource or polymorphic search, reference filter, or reference sort, including intermediate search path segments. |
282
292
  | `relationship-identifiers` | Child identifiers attached to a relationship, including has-many and AnyAPI many-to-many links. |
283
- | `relationship-validation` | A target record referenced by a write payload. |
293
+ | `relationship-validation` | A batch of up to 100 distinct target IDs referenced by a write payload, or a scoped lookup retaining database ID equality. |
284
294
  | `relationship-parent` | The parent of a relationship route such as `getRelated`. |
285
295
  | `unspecified` | A custom call to the lower-level query-filtering seam did not provide a purpose. |
286
296
 
@@ -288,12 +298,74 @@ The important invariant is not the label. It is that the predicate is attached b
288
298
 
289
299
  Treat `queryPurpose` as diagnostic context, not as permission to weaken a policy. A policy should normally add the same visibility predicate for every purpose. If an advanced policy uses a purpose-specific SQL shape, each branch must enforce equivalent visibility and must be tested independently; never skip a predicate for counts, includes, identifiers, or relationship validation.
290
300
 
301
+ Relationship identifier visibility is selected in batches of at most 100 unique
302
+ IDs per target resource type. Target query permissions and filtering hooks run
303
+ for each batch, with the caller's authorization and transaction context. Always
304
+ attach the complete predicate to the supplied builder; a request-level
305
+ "already applied" flag must not skip later queries. Keep ordering, pagination
306
+ and include limits in their operation/resource settings. Visibility hooks add
307
+ row predicates and retain the query's selected ID and mandatory constraints.
308
+ Their invocation count is not a per-request or per-record contract.
309
+
310
+ Regular and polymorphic belongs-to includes also load at most 100 target IDs
311
+ per query, with target permission and filtering hooks applied to every batch
312
+ under `queryPurpose: 'include'`. SQL projection callbacks run for each query.
313
+ Nested collection includes run after all target batches have been collected,
314
+ so a global collection limit is not restarted for each batch.
315
+
316
+ Relationship validation uses the same batch bound. Its query context has no
317
+ individual `id` and no parent client filters. Retain the selected ID and scope
318
+ predicates; filtering supplies an ID subquery used to fetch complete minimal
319
+ records. Read permission hooks then receive each target's ID and minimal record,
320
+ once per distinct identifier per relationship. Link writers do not run target
321
+ response GET hooks. See the [migration steps](33-migrating-to-v2.md#relationship-target-validation).
322
+
323
+ Large collection and relationship-metadata reads keep their selection in one
324
+ SQL query. PostgreSQL uses an array predicate and SQLite uses a JSON1 ID
325
+ subquery; their policies and projections still run before the original sort
326
+ and limit. Apply predicates using the supplied query/context rather than
327
+ assuming the internal ID constraint is always a Knex `whereIn` value array.
328
+
329
+ The identifier selection uses `DISTINCT` so predicate joins do not multiply its
330
+ returned ID rows. The library retains the input linkage order and occurrences
331
+ after visibility filtering. A permission or filter failure in a later batch
332
+ rejects the read rather than returning a partial set. No visibility results are
333
+ shared between calls, resource types or tenant APIs.
334
+
291
335
  ### Collections and counts
292
336
 
293
337
  Offset pagination applies the same mandatory policy to the data query and its count. This remains true when the caller supplies no `filters` object.
294
338
 
295
339
  Cursor predicates are added after the row policy, so the cursor moves through visible rows rather than through a page that will later be filtered.
296
340
 
341
+ Related search rows are filtered inside each joined selection. A hidden related
342
+ name cannot match a search or contribute to its count. A visible primary record
343
+ can still match its own attributes in an OR search when its related row is hidden
344
+ or absent. These selections check the target's `query` permission and use the
345
+ caller's authorization and transaction context, without inheriting the primary
346
+ resource's client filters. Explicit permission or query-hook failures reject the
347
+ query. Keep mandatory policy predicates active for `search-join` too.
348
+
349
+ Built-in filters on a belongs-to foreign key, its relationship alias, or a
350
+ polymorphic ID/type pair also require a visible target. A hidden or missing
351
+ reference behaves as null when evaluating the declared filter operator. This
352
+ applies to `actualField`, `oneOf` and joined target fields as well as direct
353
+ filters. Comparisons retain the stored column's type and collation. Custom
354
+ `applyFilter` callbacks still own the visibility of any related SQL they write;
355
+ the library cannot infer relationships from arbitrary custom predicates.
356
+
357
+ Built-in sorting on these reference fields and their aliases uses the same
358
+ visibility boundary, including `defaultSort`. Hidden and absent references
359
+ share a null sort value; later fields and the primary ID resolve ties. Ordering,
360
+ cursor comparisons and cursor values all use the same visible value, so a
361
+ hidden target's ID cannot appear in pagination metadata or links. Visible values
362
+ retain their storage column's type and collation. Target lookup predicates use
363
+ `search-join`, the current caller and the caller's transaction.
364
+
365
+ For HTTP null filters, send `filter[field][json]=null`; plain
366
+ `filter[field]=null` is the literal string. Generated links preserve typed nulls
367
+ and arrays using this explicit JSON form.
368
+
297
369
  ### Single records and writes
298
370
 
299
371
  An existing row outside the policy behaves as not found for `get`, `patch`, and `delete`. Existing-record `put` authorization uses the same scoped minimal lookup.
@@ -310,6 +382,27 @@ The target resource's policy is used when the framework loads:
310
382
  - per-parent windowed include queries
311
383
  - a relationship target during write validation
312
384
 
385
+ These secondary reads also check the target resource's `query` action permission.
386
+ A permission denial rejects the read, whereas a row policy filters individual
387
+ rows. Default relationship identifiers are subject to the same permission check
388
+ as requested includes. The target permission context retains caller auth and
389
+ transaction, with target scope/schema and no inherited parent ID or filters.
390
+
391
+ Regular-storage many-to-many reads also apply the pivot resource's permission and
392
+ policy to membership before standard/windowed include limits and linkage. AnyAPI
393
+ uses tenant-scoped canonical links for these associations; a declared pivot
394
+ resource's row policy does not apply to canonical links.
395
+
396
+ Both storage backends apply include ordering and limits after target visibility.
397
+ `include: { strategy: 'window', limit: 2, orderBy: ['name'] }` selects up to two
398
+ visible children for each parent. The default/`standard` strategy applies a global
399
+ limit to the included target set; a shared many-to-many child counts once and
400
+ remains linked to every selected parent it belongs to. Duplicate physical links
401
+ do not consume extra slots. Sparse fields and SQL query projections retain the
402
+ configured strategy. Reverse polymorphic collections follow the same rules.
403
+ Included linkage lists the selected subset; use the relationship/related endpoint
404
+ to read or paginate the full visible membership.
405
+
313
406
  Filtering the parent resource does not automatically define child visibility. Put `rowPolicy` on every child resource that has its own visibility rule.
314
407
 
315
408
  Relationship routes preserve the caller's trusted context when they invoke the target resource. Filters, sorting, and pagination supplied for a related-resource request are applied to that target query; they are deliberately withheld from the preliminary parent-visibility lookup. This keeps a target-only filter from being misinterpreted as a parent field while still enforcing the parent's policy.