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,204 +1,88 @@
1
+ // @ts-check
2
+ /** @import { FieldSelection, SelectedQuery, SelectionContext, SelectionQueryRequest, SelectionScope } from './field-selection-types.js' */
3
+ /** @import { StorageFieldDefinition } from '../storage/storage-types.js' */
1
4
  import { buildQuerySelection } from '../querying/knex-query-helpers-base.js'
2
5
  import { createSelectTranslator } from '../storage/storage-adapter.js'
3
- import { buildQueryFieldRuntimes } from './query-field-helpers.js'
6
+ import { buildQueryFieldRuntimes, normalizeStructuredSelect } from './query-field-helpers.js'
4
7
  import { RestApiFieldsetError } from '../../../../lib/rest-api-errors.js'
5
-
6
- /**
7
- * Checks if a field exists only in code, not in the database
8
- *
9
- * @param {string} fieldName - The field name to check
10
- * @param {Object} schemaInfo - Schema info with computed fields and structure
11
- * @returns {boolean} True if field is computed or virtual
12
- *
13
- * @example
14
- * // Input: Check computed field
15
- * const schemaInfo = {
16
- * computed: { profit_margin: { (definition) } },
17
- * schemaStructure: { price: { type: 'number' } }
18
- * };
19
- * isNonDatabaseField('profit_margin', schemaInfo);
20
- * // Output: true (it's computed, not stored)
21
- *
22
- * @example
23
- * // Input: Check virtual field
24
- * const schemaInfo = {
25
- * computed: {},
26
- * schemaStructure: {
27
- * temp_password: { type: 'string', virtual: true }
28
- * }
29
- * };
30
- * isNonDatabaseField('temp_password', schemaInfo);
31
- * // Output: true (it's virtual, not stored)
32
- *
33
- * @description
34
- * Used by:
35
- * - buildFieldSelection to exclude from SELECT queries
36
- * - dataPost/dataPatch to skip non-database fields
37
- * - enrichAttributes to identify computed fields
38
- *
39
- * Purpose:
40
- * - Prevents SQL errors by excluding non-existent columns
41
- * - Identifies fields that need computation or special handling
42
- * - Supports virtual fields for temporary/input-only data
43
- */
44
- export const isNonDatabaseField = (fieldName, schemaInfo) => {
45
- const { computed = {}, schemaStructure = {} } = schemaInfo
46
- if (fieldName in computed) return true
47
- const fieldDef = schemaStructure[fieldName]
48
- return fieldDef && fieldDef.virtual === true
8
+ import { getResourceFieldset, parseFieldset } from './field-utils.js'
9
+ import { databaseIdentityExpression } from './database-value-normalizers.js'
10
+ import { findRelationshipDefinition } from './relationship-contracts.js'
11
+ import { wrapUnexpectedError } from '../../../../lib/error-context.js'
12
+ import { getFieldDependencyClosure } from './schema-helpers.js'
13
+ import { resolveSortField } from '../querying/query-field-sort-helpers.js'
14
+
15
+ /** @param {SelectionContext} context @param {Record<string, SelectionScope>} scopes */
16
+ export const validateRequestedFieldsets = async (context, scopes) => {
17
+ for (const scopeName of Object.keys(context.queryParams?.fields || {})) {
18
+ if (!Object.hasOwn(scopes, scopeName) || !scopes[scopeName]?.vars?.schemaInfo) {
19
+ throw new RestApiFieldsetError({ resourceType: scopeName })
20
+ }
21
+ await buildFieldSelection(scopes[scopeName], {
22
+ context: { scopeName, queryParams: { fields: context.queryParams?.fields } }
23
+ })
24
+ }
49
25
  }
50
26
 
51
27
  /**
52
- * Builds the field selection list for database queries with sparse fieldset support
53
- *
54
- * @param {Object} scope - Resource scope with schema information
55
- * @param {Object} deps - Dependencies object
56
- * @param {Object} deps.context - Request context with queryParams and schemaInfo
57
- * @returns {Promise<Object>} Field selection information
58
- *
59
- * @example
60
- * // Input: Basic sparse fieldset request
61
- * const deps = {
62
- * context: {
63
- * scopeName: 'articles',
64
- * queryParams: { fields: { articles: 'title,body' } },
65
- * schemaInfo: { idProperty: 'id', ... }
66
- * }
67
- * };
68
- * const result = await buildFieldSelection(scope, deps);
69
- *
70
- * // Output: Includes requested fields plus required fields
71
- * // {
72
- * // fieldsToSelect: ['id', 'title', 'body', 'author_id', 'category_id'],
73
- * // requestedFields: ['title', 'body'],
74
- * // computedDependencies: [],
75
- * // idProperty: 'id'
76
- * // }
77
- * // Note: Foreign keys (author_id, category_id) always included for relationships
78
- *
79
- * @example
80
- * // Input: Computed field with hidden dependencies
81
- * const schemaInfo = {
82
- * computed: {
83
- * profit_margin: {
84
- * compute: (record) => (record.price - record.cost) / record.price,
85
- * dependencies: ['price', 'cost']
86
- * }
87
- * },
88
- * schemaStructure: {
89
- * name: { type: 'string' },
90
- * price: { type: 'number' },
91
- * cost: { type: 'number', normallyHidden: true }
92
- * }
93
- * };
94
- * const deps = {
95
- * context: {
96
- * queryParams: { fields: { products: 'name,profit_margin' } }
97
- * }
98
- * };
99
- *
100
- * // Output: Fetches hidden dependency for computation
101
- * // {
102
- * // fieldsToSelect: ['id', 'name', 'price', 'cost'],
103
- * // requestedFields: ['name', 'profit_margin'],
104
- * // computedDependencies: ['cost'], // Will be removed after computation
105
- * // idProperty: 'id'
106
- * // }
107
- *
108
- * @example
109
- * // Input: No sparse fieldsets (returns all visible fields)
110
- * const deps = {
111
- * context: {
112
- * queryParams: {}, // No fields parameter
113
- * schemaInfo: { ... }
114
- * }
115
- * };
116
- *
117
- * // Output: All non-hidden, non-virtual fields
118
- * // {
119
- * // fieldsToSelect: ['id', 'title', 'body', 'published_at', 'author_id'],
120
- * // requestedFields: null,
121
- * // computedDependencies: [],
122
- * // idProperty: 'id'
123
- * // }
124
- *
125
- * @description
126
- * Used by:
127
- * - dataGet to build SELECT query
128
- * - dataQuery to build SELECT query
129
- * - buildQuerySelection as the main field resolver
130
- *
131
- * Purpose:
132
- * - Implements JSON:API sparse fieldsets specification
133
- * - Ensures relationships work by including foreign keys
134
- * - Handles computed field dependencies intelligently
135
- * - Respects field visibility rules (hidden/normallyHidden)
136
- * - Supports custom ID column names
137
- *
138
- * Data flow:
139
- * 1. Always includes ID (with aliasing if needed)
140
- * 2. Parses requested fields from query string
141
- * 3. Validates requested fields exist in schema
142
- * 4. Adds dependencies for computed fields
143
- * 5. Includes all foreign keys for relationships
144
- * 6. Returns complete field list for SQL SELECT
28
+ * Resolve public fieldsets to logical storage/projection names and read dependencies.
29
+ * Always fetch the logical ID and visible relationship backing fields. Fetch sort
30
+ * values needed by cursors and hidden dependencies needed by selected callbacks;
31
+ * response formatting owns removal of values that were only read dependencies.
32
+ * This prepares selection metadata without executing a query or completing a transaction.
33
+ * @param {SelectionScope | null | undefined} scope
34
+ * @param {{ context: SelectionContext }} deps
35
+ * @returns {Promise<FieldSelection>}
145
36
  */
146
37
  export const buildFieldSelection = async (scope, deps) => {
38
+ /** @type {Set<string>} */
147
39
  const fieldsToSelect = new Set()
40
+ /** @type {Set<string>} */
148
41
  const computedDependencies = new Set()
149
42
 
150
43
  // Extract values from scope
151
44
  const scopeSchemaInfo = scope?.vars?.schemaInfo || {}
152
45
  const {
153
- schemaInstance,
46
+ schemaStructure = {},
154
47
  computed: computedFields = {},
155
48
  idProperty = 'id'
156
49
  } = scopeSchemaInfo
157
- let schemaStructure = scopeSchemaInfo.schemaStructure
158
- const queryFields = scope?.vars?.queryFields || {}
50
+ const queryFields = scope?.vars?.schemaInfo?.queryFields || {}
159
51
 
160
52
  // Extract values from deps
161
53
  const { context } = deps
162
54
  const scopeName = context.scopeName
163
- const requestedFields = context.queryParams?.fields?.[scopeName]
55
+ const requestedFields = getResourceFieldset(context.queryParams?.fields, scopeName)
164
56
 
165
57
  // Always include the logical ID field - the storage adapter translates it.
166
58
  fieldsToSelect.add('id')
167
59
 
168
- // Handle both Schema objects and plain objects
169
- if (!schemaStructure) {
170
- schemaStructure = schemaInstance?.structure || schemaInstance || {}
171
- }
172
-
173
60
  // Get computed fields and virtual fields from schema
174
61
  const computedFieldNames = new Set(Object.keys(computedFields))
175
62
  const queryFieldNames = new Set(Object.keys(queryFields))
63
+ /** @type {Set<string>} */
176
64
  const queryFieldsToSelect = new Set()
177
65
 
178
66
  // Find fields marked as virtual in the schema
179
- const virtualFieldNames = new Set()
67
+ /** @type {Map<string, StorageFieldDefinition>} */
68
+ const virtualFields = new Map()
180
69
  Object.entries(schemaStructure).forEach(([fieldName, fieldDef]) => {
181
70
  if (fieldDef.virtual === true) {
182
- virtualFieldNames.add(fieldName)
71
+ virtualFields.set(fieldName, fieldDef)
183
72
  }
184
73
  })
185
74
 
186
- const nonDatabaseFields = new Set([...computedFieldNames, ...virtualFieldNames])
75
+ const nonDatabaseFields = new Set([...computedFieldNames, ...virtualFields.keys()])
187
76
 
188
77
  // Parse requested fields
189
- const requested = requestedFields
190
- ? (
191
- typeof requestedFields === 'string'
192
- ? requestedFields.split(',').map(f => f.trim()).filter(f => f)
193
- : requestedFields
194
- )
195
- : null
196
-
197
- if (requested && requested.length > 0) {
78
+ const requested = parseFieldset(requestedFields)
79
+
80
+ if (requested !== null) {
198
81
  // Sparse fieldsets requested - only select specified fields
199
82
  // Example: ?fields[products]=name,price,profit_margin
200
83
  requested.forEach(field => {
201
84
  if (field === 'id' || field === idProperty) return
85
+ if (findRelationshipDefinition(scopeSchemaInfo, field)) return
202
86
 
203
87
  if (queryFieldNames.has(field)) {
204
88
  if (queryFields[field]?.hidden === true) return
@@ -212,7 +96,7 @@ export const buildFieldSelection = async (scope, deps) => {
212
96
  if (nonDatabaseFields.has(field)) return
213
97
 
214
98
  const fieldDef = schemaStructure[field]
215
- if (!fieldDef) {
99
+ if (!Object.hasOwn(schemaStructure, field) || !fieldDef) {
216
100
  throw new RestApiFieldsetError({
217
101
  field,
218
102
  resourceType: scopeName
@@ -229,43 +113,6 @@ export const buildFieldSelection = async (scope, deps) => {
229
113
 
230
114
  fieldsToSelect.add(field)
231
115
  })
232
-
233
- // Handle computed field dependencies - fetch fields needed for calculations
234
- // Example: User requests 'profit_margin' which depends on 'price' and 'cost'
235
- // We need to fetch price and cost from DB even if not explicitly requested
236
- const requestedComputedFields = requested.filter(f => computedFieldNames.has(f))
237
- for (const computedField of requestedComputedFields) {
238
- const fieldDef = computedFields[computedField]
239
- if (fieldDef.dependencies) {
240
- for (const dep of fieldDef.dependencies) {
241
- const depFieldDef = schemaStructure[dep]
242
- // Only add dependency if it exists and isn't hidden
243
- if (depFieldDef && depFieldDef.hidden !== true) {
244
- fieldsToSelect.add(dep)
245
- // Track dependencies that weren't explicitly requested
246
- // These will be removed from the final response
247
- if (!requested.includes(dep)) {
248
- computedDependencies.add(dep)
249
- }
250
- }
251
- }
252
- }
253
- }
254
-
255
- // Still handle normallyHidden fields for backward compatibility
256
- if (requestedComputedFields.length > 0) {
257
- Object.entries(schemaStructure).forEach(([field, fieldDef]) => {
258
- if (fieldDef.normallyHidden === true && fieldDef.hidden !== true) {
259
- // Only add if not already handled by dependencies
260
- if (!fieldsToSelect.has(field)) {
261
- fieldsToSelect.add(field)
262
- if (!requested.includes(field)) {
263
- computedDependencies.add(field)
264
- }
265
- }
266
- }
267
- })
268
- }
269
116
  } else {
270
117
  // No sparse fieldsets - return all visible fields
271
118
  // This is the default behavior when no ?fields parameter is provided
@@ -289,26 +136,6 @@ export const buildFieldSelection = async (scope, deps) => {
289
136
  fieldsToSelect.add(field)
290
137
  })
291
138
 
292
- // When no sparse fieldsets, we compute all computed fields
293
- // So we need to include their dependencies even if normallyHidden
294
- // Example: profit_margin depends on 'cost' which is normallyHidden
295
- // We fetch 'cost' for calculation but don't return it in response
296
- for (const [, fieldDef] of Object.entries(computedFields)) {
297
- if (fieldDef.dependencies) {
298
- for (const dep of fieldDef.dependencies) {
299
- const depFieldDef = schemaStructure[dep]
300
- if (depFieldDef && depFieldDef.hidden !== true) {
301
- fieldsToSelect.add(dep)
302
- // Track normallyHidden dependencies for later removal
303
- // These are fetched for computation but not returned
304
- if (depFieldDef.normallyHidden === true) {
305
- computedDependencies.add(dep)
306
- }
307
- }
308
- }
309
- }
310
- }
311
-
312
139
  for (const [fieldName, fieldDef] of Object.entries(queryFields)) {
313
140
  if (fieldDef.hidden === true || fieldDef.normallyHidden === true) continue
314
141
  queryFieldsToSelect.add(fieldName)
@@ -321,10 +148,15 @@ export const buildFieldSelection = async (scope, deps) => {
321
148
 
322
149
  for (const sortField of sortFields) {
323
150
  if (typeof sortField !== 'string') continue
324
- const normalizedField = sortField.startsWith('-') ? sortField.slice(1) : sortField
151
+ const normalizedField = resolveSortField(sortField.startsWith('-') ? sortField.slice(1) : sortField, scopeSchemaInfo)
325
152
 
326
153
  if (queryFieldNames.has(normalizedField)) {
327
154
  queryFieldsToSelect.add(normalizedField)
155
+ } else if (schemaStructure[normalizedField] &&
156
+ schemaStructure[normalizedField].hidden !== true &&
157
+ !nonDatabaseFields.has(normalizedField)) {
158
+ // Cursor generation needs sort values even when the response omits them.
159
+ fieldsToSelect.add(normalizedField)
328
160
  }
329
161
  }
330
162
 
@@ -337,15 +169,35 @@ export const buildFieldSelection = async (scope, deps) => {
337
169
 
338
170
  // Always include polymorphic type and id fields from relationships
339
171
  try {
340
- const relationships = scope.vars.schemaInfo.schemaRelationships
172
+ const relationships = scopeSchemaInfo.schemaRelationships
341
173
  Object.entries(relationships || {}).forEach(([relName, relDef]) => {
342
174
  if (relDef.belongsToPolymorphic) {
343
- if (relDef.typeField) fieldsToSelect.add(relDef.typeField)
344
- if (relDef.idField) fieldsToSelect.add(relDef.idField)
175
+ fieldsToSelect.add(relDef.belongsToPolymorphic.typeField)
176
+ fieldsToSelect.add(relDef.belongsToPolymorphic.idField)
345
177
  }
346
178
  })
347
- } catch (e) {
348
- // Scope might not have relationships
179
+ } catch (error) {
180
+ throw wrapUnexpectedError(error, {
181
+ message: `Failed to select relationship fields for '${scopeName}'`,
182
+ context: { scopeName, phase: 'relationshipMetadata' }
183
+ })
184
+ }
185
+
186
+ const selectedFields = new Set([
187
+ ...fieldsToSelect,
188
+ ...queryFieldsToSelect,
189
+ ...getRequestedComputedFields(scopeName, requestedFields, computedFields)
190
+ ])
191
+ for (const [field, definition] of virtualFields) {
192
+ if (definition.hidden !== true && (requested === null ? definition.normallyHidden !== true : requested.includes(field))) {
193
+ selectedFields.add(field)
194
+ }
195
+ }
196
+ for (const field of getFieldDependencyClosure(scopeSchemaInfo, selectedFields)) {
197
+ if (field === 'id' || field === idProperty || nonDatabaseFields.has(field)) continue
198
+ if (queryFieldNames.has(field)) queryFieldsToSelect.add(field)
199
+ else if (Object.hasOwn(schemaStructure, field)) fieldsToSelect.add(field)
200
+ if (!selectedFields.has(field)) computedDependencies.add(field)
349
201
  }
350
202
 
351
203
  // Return detailed information about field selection
@@ -362,6 +214,7 @@ export const buildFieldSelection = async (scope, deps) => {
362
214
  }
363
215
  }
364
216
 
217
+ /** @param {SelectionQueryRequest} options @returns {Promise<SelectedQuery>} */
365
218
  export const applyFieldSelectionToQuery = async ({
366
219
  query,
367
220
  scope,
@@ -374,18 +227,34 @@ export const applyFieldSelectionToQuery = async ({
374
227
  scopeName = ''
375
228
  }) => {
376
229
  const selectTranslator = createSelectTranslator(storageAdapter)
230
+ const schemaInfo = scope?.vars?.schemaInfo || {}
231
+ const foreignKeyFields = schemaInfo.foreignKeyFields
232
+ /** @param {string} field @param {string | null} [alias] */
233
+ const translateColumn = (field, alias) => {
234
+ const translated = selectTranslator?.(field, alias) || (alias ? `${alias}.${field}` : field)
235
+ if (db && !storageAdapter?.isCanonical() && (field === 'id' || foreignKeyFields?.has(field))) {
236
+ const column = storageAdapter?.translateColumn(field) || field
237
+ const outputName = field === 'id' ? 'id' : column
238
+ const qualifiedColumn = alias ? `${alias}.${column}` : column
239
+ return { [outputName]: databaseIdentityExpression(db, qualifiedColumn) }
240
+ }
241
+ const expression = normalizeStructuredSelect(translated, scope?.vars?.schemaInfo?.schemaStructure?.[field], db)
242
+ if (expression === translated) return translated
243
+ const outputName = storageAdapter?.translateColumn(field) || field
244
+ return { [outputName]: expression }
245
+ }
377
246
 
378
247
  const selectedQuery = buildQuerySelection(
379
248
  query,
380
249
  tableName,
381
250
  fieldSelectionInfo?.fieldsToSelect || [],
382
251
  useTablePrefix,
383
- selectTranslator ? { translateColumn: selectTranslator } : undefined
252
+ { translateColumn }
384
253
  )
385
254
 
386
255
  const queryFieldRuntimeByField = await buildQueryFieldRuntimes({
387
256
  queryFieldNames: fieldSelectionInfo?.queryFieldsToSelect || [],
388
- queryFields: scope?.vars?.queryFields || {},
257
+ queryFields: scope?.vars?.schemaInfo?.queryFields || {},
389
258
  schemaInfo: scope?.vars?.schemaInfo || {},
390
259
  tableName,
391
260
  storageAdapter,
@@ -405,72 +274,27 @@ export const applyFieldSelectionToQuery = async ({
405
274
  }
406
275
 
407
276
  /**
408
- * Determines which computed fields to calculate based on sparse fieldsets
409
- *
410
- * @param {string} scopeName - Resource name (e.g., 'products')
411
- * @param {Array<string>|string} requestedFields - Requested fields from query
412
- * @param {Object} computedFields - Computed field definitions from schema
413
- * @returns {Array<string>} Names of computed fields to calculate
414
- *
415
- * @example
416
- * // Input: No sparse fieldsets (calculate all computed fields)
417
- * const computed = {
418
- * full_name: { compute: (r) => `${r.first} ${r.last}` },
419
- * age: { compute: (r) => new Date().getFullYear() - r.birth_year }
420
- * };
421
- * getRequestedComputedFields('users', null, computed);
422
- * // Output: ['full_name', 'age'] (all computed fields)
423
- *
424
- * @example
425
- * // Input: Sparse fieldsets with some computed fields
426
- * const requestedFields = 'first_name,full_name,email';
427
- * const computed = {
428
- * full_name: { compute: (r) => `${r.first} ${r.last}` },
429
- * age: { compute: (r) => new Date().getFullYear() - r.birth_year }
430
- * };
431
- * getRequestedComputedFields('users', requestedFields, computed);
432
- * // Output: ['full_name'] (only requested computed field)
433
- *
434
- * @example
435
- * // Input: Sparse fieldsets with no computed fields
436
- * const requestedFields = ['first_name', 'email'];
437
- * const computed = {
438
- * full_name: { compute: (r) => `${r.first} ${r.last}` }
439
- * };
440
- * getRequestedComputedFields('users', requestedFields, computed);
441
- * // Output: [] (no computed fields requested)
442
- *
443
- * @description
444
- * Used by:
445
- * - enrichAttributes to determine which computations to run
446
- * - Applied after fetching data from database
447
- *
448
- * Purpose:
449
- * - Optimizes performance by only computing requested fields
450
- * - Supports JSON:API sparse fieldsets for computed fields
451
- * - Handles both string and array input formats
452
- *
453
- * Data flow:
454
- * 1. Checks if sparse fieldsets are specified
455
- * 2. If not, returns all computed field names
456
- * 3. If yes, filters to only requested computed fields
457
- * 4. Normalizes string input to array format
277
+ * Select visible computed fields in request order, or declaration order when
278
+ * no fieldset is supplied. Explicit selection includes normally-hidden fields.
279
+ * @param {string} scopeName
280
+ * @param {string | readonly string[] | null | undefined} requestedFields
281
+ * @param {Record<string, { hidden?: boolean, normallyHidden?: boolean }> | undefined} computedFields
282
+ * @returns {string[]}
458
283
  */
459
284
  export const getRequestedComputedFields = (scopeName, requestedFields, computedFields) => {
460
285
  if (!computedFields) return []
461
286
 
462
- const allComputedFields = Object.keys(computedFields)
463
-
464
- if (!requestedFields || requestedFields.length === 0) {
465
- // No sparse fieldsets - return all computed fields
466
- return allComputedFields
287
+ const requested = parseFieldset(requestedFields)
288
+ if (requested === null) {
289
+ return Object.entries(computedFields).filter(([, definition]) => {
290
+ return definition.hidden !== true && definition.normallyHidden !== true
291
+ }).map(([field]) => field)
467
292
  }
468
293
 
469
- // Parse requested fields if it's a string
470
- const requested = typeof requestedFields === 'string'
471
- ? requestedFields.split(',').map(f => f.trim()).filter(f => f)
472
- : requestedFields
473
-
474
- // Return only requested computed fields that exist
475
- return requested.filter(field => allComputedFields.includes(field))
294
+ return requested.filter(field => {
295
+ if (!Object.hasOwn(computedFields, field)) return false
296
+ const definition = computedFields[field]
297
+ if (!definition) throw new Error(`Missing compiled computed definition '${scopeName}.${field}'`)
298
+ return definition.hidden !== true
299
+ })
476
300
  }