json-rest-api 1.0.29 → 2.1.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 (294) 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 +733 -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 +103 -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 +3838 -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 +37 -0
  44. package/index.js +3 -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 +70 -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 +586 -418
  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 +120 -56
  71. package/plugins/core/lib/anyapi/stored-definition.js +109 -0
  72. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  73. package/plugins/core/lib/dbIntrospection.js +228 -21
  74. package/plugins/core/lib/dbTablesOperations.js +409 -92
  75. package/plugins/core/lib/fractional-positioning.js +12 -48
  76. package/plugins/core/lib/querying/identifier-query.js +19 -0
  77. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  78. package/plugins/core/lib/querying/include-to-many.js +428 -0
  79. package/plugins/core/lib/querying/include-to-one.js +579 -0
  80. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  81. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  82. package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
  83. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  84. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  85. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  86. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  87. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  88. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  89. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  90. package/plugins/core/lib/querying/query-constraint.js +27 -0
  91. package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
  92. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  93. package/plugins/core/lib/querying/sort-helpers.js +13 -1
  94. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  95. package/plugins/core/lib/querying/url-helpers.js +11 -4
  96. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  97. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  98. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  99. package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
  100. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  101. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  102. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  103. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  104. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  105. package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
  106. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  107. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  108. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  109. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  110. package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
  111. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  112. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  113. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  114. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  115. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  116. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  117. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  118. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  119. package/plugins/core/lib/storage/storage-adapter.js +128 -62
  120. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  121. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  122. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  123. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  124. package/plugins/core/lib/writing/resource-version.js +172 -0
  125. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  126. package/plugins/core/rest-api-anyapi-knex-plugin.js +290 -2475
  127. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  128. package/plugins/core/rest-api-cors-plugin.js +52 -205
  129. package/plugins/core/rest-api-knex-plugin.js +45 -1077
  130. package/plugins/core/rest-api-label-plugin.js +25 -38
  131. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  132. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  133. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  134. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  135. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  136. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  137. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  138. package/plugins/core/rest-api-plugin-methods/common.js +291 -395
  139. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  140. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  141. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
  142. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  143. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  144. package/plugins/core/rest-api-plugin-methods/get.js +36 -68
  145. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  146. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  147. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  148. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  149. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  150. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  151. package/plugins/core/rest-api-plugin-methods/query.js +37 -81
  152. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  153. package/plugins/core/rest-api-plugin.js +32 -46
  154. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  155. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  156. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  157. package/plugins/core/socketio-plugin.js +364 -344
  158. package/plugins/storage/local-storage.js +40 -36
  159. package/types/anyapi.d.ts +30 -0
  160. package/types/autofilter.d.ts +57 -0
  161. package/types/bulk-methods.d.ts +63 -0
  162. package/types/errors.d.ts +118 -0
  163. package/types/fastify-options.d.ts +19 -0
  164. package/types/file-detectors.d.ts +33 -0
  165. package/types/file-storage.d.ts +48 -0
  166. package/types/hook-context.d.ts +180 -0
  167. package/types/plugin-options.d.ts +65 -0
  168. package/types/relationship-methods.d.ts +74 -0
  169. package/types/representations.d.ts +50 -0
  170. package/types/resource-methods.d.ts +95 -0
  171. package/types/resource-schema.d.ts +157 -0
  172. package/types/row-policy.d.ts +31 -0
  173. package/types/runtime.d.ts +132 -0
  174. package/types/transactions.d.ts +21 -0
  175. package/.claude/agents/library-guide-expert.md +0 -30
  176. package/.claude/agents/rest-api-internals-expert.md +0 -54
  177. package/.claude/settings.local.json +0 -70
  178. package/AGENTS.md +0 -34
  179. package/CLAUDE.md +0 -160
  180. package/CRUSH.md +0 -55
  181. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  182. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  183. package/TODO.todo +0 -13
  184. package/connectors_ideas/fastify.js +0 -217
  185. package/connectors_ideas/koa-plugin.js +0 -112
  186. package/docs/.bundle/config +0 -2
  187. package/docs/COMPARISON.md +0 -433
  188. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  189. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  190. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  191. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  192. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  193. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  194. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  195. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
  196. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  197. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  198. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  199. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  200. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  201. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  202. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  203. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  204. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  205. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  206. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  207. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  208. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  209. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  210. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  211. package/docs/Gemfile +0 -6
  212. package/docs/Gemfile.lock +0 -308
  213. package/docs/ONBOARDING.md +0 -315
  214. package/docs/_config.yml +0 -36
  215. package/docs/_layouts/default.html +0 -123
  216. package/docs/apple-touch-icon.svg +0 -4
  217. package/docs/assets/css/style.scss +0 -392
  218. package/docs/favicon.svg +0 -4
  219. package/docs/index.md +0 -164
  220. package/docs/mini-guides/file-uploads-complete.md +0 -161
  221. package/eslint.config.js +0 -13
  222. package/examples/url-override-example.js +0 -137
  223. package/fixing_plan.md +0 -112
  224. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  225. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  226. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  227. package/plugins/storage/s3-storage.js +0 -93
  228. package/quickTest.js +0 -136
  229. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  230. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  231. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  232. package/scripts/run-docs.js +0 -133
  233. package/test-debug.js +0 -59
  234. package/tests/README.md +0 -202
  235. package/tests/TEST_TEMPLATE.test.js +0 -178
  236. package/tests/anyapi-basic.test.js +0 -105
  237. package/tests/anyapi-cursor-pagination.test.js +0 -148
  238. package/tests/anyapi-custom-idproperty.test.js +0 -163
  239. package/tests/autofilter.test.js +0 -463
  240. package/tests/bulk-operations.test.js +0 -657
  241. package/tests/computed-fields.test.js +0 -586
  242. package/tests/cors.test.js +0 -834
  243. package/tests/curl.test.js +0 -467
  244. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  245. package/tests/custom-id-simplified-post.test.js +0 -75
  246. package/tests/custom-idproperty-relationships.test.js +0 -932
  247. package/tests/db-introspection.test.js +0 -521
  248. package/tests/db-table-operations.test.js +0 -798
  249. package/tests/fastify-plugin.test.js +0 -184
  250. package/tests/field-getters.test.js +0 -370
  251. package/tests/field-setters.test.js +0 -306
  252. package/tests/file-handling.test.js +0 -155
  253. package/tests/fixtures/api-configs.js +0 -2566
  254. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  255. package/tests/helpers/fake-fastify.js +0 -99
  256. package/tests/helpers/storage-mode.js +0 -65
  257. package/tests/helpers/test-utils.js +0 -203
  258. package/tests/http-connectors-parity.test.js +0 -395
  259. package/tests/id-normalization.test.js +0 -444
  260. package/tests/include-depth-validation.test.js +0 -331
  261. package/tests/includes.test.js +0 -427
  262. package/tests/local-storage.test.js +0 -96
  263. package/tests/logical-id-write-validation.test.js +0 -151
  264. package/tests/nested-includes.test.js +0 -617
  265. package/tests/pagination-boundaries.test.js +0 -154
  266. package/tests/pagination-contract.test.js +0 -122
  267. package/tests/pagination-cursor-multifield.test.js +0 -641
  268. package/tests/pagination-enhanced.test.js +0 -563
  269. package/tests/polymorphic-relationships.test.js +0 -347
  270. package/tests/polymorphic-simple.test.js +0 -148
  271. package/tests/positioning-advanced.test.js +0 -692
  272. package/tests/positioning-extended.test.js +0 -915
  273. package/tests/positioning-multifilter.test.js +0 -699
  274. package/tests/positioning.test.js +0 -560
  275. package/tests/post-commit-hooks.test.js +0 -120
  276. package/tests/projected-fields.test.js +0 -319
  277. package/tests/public-exports.test.js +0 -15
  278. package/tests/put-create-parity.test.js +0 -216
  279. package/tests/queries.test.js +0 -521
  280. package/tests/query-limits.test.js +0 -584
  281. package/tests/relationship-endpoints.test.js +0 -620
  282. package/tests/relationship-include-storage-adapter.test.js +0 -56
  283. package/tests/relationships.test.js +0 -681
  284. package/tests/rest-api.test.js +0 -337
  285. package/tests/return-record-settings.test.js +0 -184
  286. package/tests/row-policy.test.js +0 -489
  287. package/tests/s3-storage.test.js +0 -38
  288. package/tests/searchschema-knex-behavior.test.js +0 -187
  289. package/tests/searchschema-merge.test.js +0 -151
  290. package/tests/socketio.test.js +0 -860
  291. package/tests/storage-mapping.test.js +0 -362
  292. package/tests/temporal-boundaries.test.js +0 -208
  293. package/tests/test-development-guide.md +0 -370
  294. package/tests/virtual-fields.test.js +0 -404
@@ -1,213 +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
-
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 */
6
16
  export const validateRequestedFieldsets = async (context, scopes) => {
7
17
  for (const scopeName of Object.keys(context.queryParams?.fields || {})) {
8
- if (!scopes[scopeName]?.vars?.schemaInfo) continue
18
+ if (!Object.hasOwn(scopes, scopeName) || !scopes[scopeName]?.vars?.schemaInfo) {
19
+ throw new RestApiFieldsetError({ resourceType: scopeName })
20
+ }
9
21
  await buildFieldSelection(scopes[scopeName], {
10
- context: { scopeName, queryParams: { fields: context.queryParams.fields } }
22
+ context: { scopeName, queryParams: { fields: context.queryParams?.fields } }
11
23
  })
12
24
  }
13
25
  }
14
26
 
15
27
  /**
16
- * Checks if a field exists only in code, not in the database
17
- *
18
- * @param {string} fieldName - The field name to check
19
- * @param {Object} schemaInfo - Schema info with computed fields and structure
20
- * @returns {boolean} True if field is computed or virtual
21
- *
22
- * @example
23
- * // Input: Check computed field
24
- * const schemaInfo = {
25
- * computed: { profit_margin: { (definition) } },
26
- * schemaStructure: { price: { type: 'number' } }
27
- * };
28
- * isNonDatabaseField('profit_margin', schemaInfo);
29
- * // Output: true (it's computed, not stored)
30
- *
31
- * @example
32
- * // Input: Check virtual field
33
- * const schemaInfo = {
34
- * computed: {},
35
- * schemaStructure: {
36
- * temp_password: { type: 'string', virtual: true }
37
- * }
38
- * };
39
- * isNonDatabaseField('temp_password', schemaInfo);
40
- * // Output: true (it's virtual, not stored)
41
- *
42
- * @description
43
- * Used by:
44
- * - buildFieldSelection to exclude from SELECT queries
45
- * - dataPost/dataPatch to skip non-database fields
46
- * - enrichAttributes to identify computed fields
47
- *
48
- * Purpose:
49
- * - Prevents SQL errors by excluding non-existent columns
50
- * - Identifies fields that need computation or special handling
51
- * - Supports virtual fields for temporary/input-only data
52
- */
53
- export const isNonDatabaseField = (fieldName, schemaInfo) => {
54
- const { computed = {}, schemaStructure = {} } = schemaInfo
55
- if (fieldName in computed) return true
56
- const fieldDef = schemaStructure[fieldName]
57
- return fieldDef && fieldDef.virtual === true
58
- }
59
-
60
- /**
61
- * Builds the field selection list for database queries with sparse fieldset support
62
- *
63
- * @param {Object} scope - Resource scope with schema information
64
- * @param {Object} deps - Dependencies object
65
- * @param {Object} deps.context - Request context with queryParams and schemaInfo
66
- * @returns {Promise<Object>} Field selection information
67
- *
68
- * @example
69
- * // Input: Basic sparse fieldset request
70
- * const deps = {
71
- * context: {
72
- * scopeName: 'articles',
73
- * queryParams: { fields: { articles: 'title,body' } },
74
- * schemaInfo: { idProperty: 'id', ... }
75
- * }
76
- * };
77
- * const result = await buildFieldSelection(scope, deps);
78
- *
79
- * // Output: Includes requested fields plus required fields
80
- * // {
81
- * // fieldsToSelect: ['id', 'title', 'body', 'author_id', 'category_id'],
82
- * // requestedFields: ['title', 'body'],
83
- * // computedDependencies: [],
84
- * // idProperty: 'id'
85
- * // }
86
- * // Note: Foreign keys (author_id, category_id) always included for relationships
87
- *
88
- * @example
89
- * // Input: Computed field with hidden dependencies
90
- * const schemaInfo = {
91
- * computed: {
92
- * profit_margin: {
93
- * compute: (record) => (record.price - record.cost) / record.price,
94
- * dependencies: ['price', 'cost']
95
- * }
96
- * },
97
- * schemaStructure: {
98
- * name: { type: 'string' },
99
- * price: { type: 'number' },
100
- * cost: { type: 'number', normallyHidden: true }
101
- * }
102
- * };
103
- * const deps = {
104
- * context: {
105
- * queryParams: { fields: { products: 'name,profit_margin' } }
106
- * }
107
- * };
108
- *
109
- * // Output: Fetches hidden dependency for computation
110
- * // {
111
- * // fieldsToSelect: ['id', 'name', 'price', 'cost'],
112
- * // requestedFields: ['name', 'profit_margin'],
113
- * // computedDependencies: ['cost'], // Will be removed after computation
114
- * // idProperty: 'id'
115
- * // }
116
- *
117
- * @example
118
- * // Input: No sparse fieldsets (returns all visible fields)
119
- * const deps = {
120
- * context: {
121
- * queryParams: {}, // No fields parameter
122
- * schemaInfo: { ... }
123
- * }
124
- * };
125
- *
126
- * // Output: All non-hidden, non-virtual fields
127
- * // {
128
- * // fieldsToSelect: ['id', 'title', 'body', 'published_at', 'author_id'],
129
- * // requestedFields: null,
130
- * // computedDependencies: [],
131
- * // idProperty: 'id'
132
- * // }
133
- *
134
- * @description
135
- * Used by:
136
- * - dataGet to build SELECT query
137
- * - dataQuery to build SELECT query
138
- * - buildQuerySelection as the main field resolver
139
- *
140
- * Purpose:
141
- * - Implements JSON:API sparse fieldsets specification
142
- * - Ensures relationships work by including foreign keys
143
- * - Handles computed field dependencies intelligently
144
- * - Respects field visibility rules (hidden/normallyHidden)
145
- * - Supports custom ID column names
146
- *
147
- * Data flow:
148
- * 1. Always includes ID (with aliasing if needed)
149
- * 2. Parses requested fields from query string
150
- * 3. Validates requested fields exist in schema
151
- * 4. Adds dependencies for computed fields
152
- * 5. Includes all foreign keys for relationships
153
- * 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>}
154
36
  */
155
37
  export const buildFieldSelection = async (scope, deps) => {
38
+ /** @type {Set<string>} */
156
39
  const fieldsToSelect = new Set()
40
+ /** @type {Set<string>} */
157
41
  const computedDependencies = new Set()
158
42
 
159
43
  // Extract values from scope
160
44
  const scopeSchemaInfo = scope?.vars?.schemaInfo || {}
161
45
  const {
162
- schemaInstance,
46
+ schemaStructure = {},
163
47
  computed: computedFields = {},
164
48
  idProperty = 'id'
165
49
  } = scopeSchemaInfo
166
- let schemaStructure = scopeSchemaInfo.schemaStructure
167
- const queryFields = scope?.vars?.queryFields || {}
50
+ const queryFields = scope?.vars?.schemaInfo?.queryFields || {}
168
51
 
169
52
  // Extract values from deps
170
53
  const { context } = deps
171
54
  const scopeName = context.scopeName
172
- const requestedFields = context.queryParams?.fields?.[scopeName]
55
+ const requestedFields = getResourceFieldset(context.queryParams?.fields, scopeName)
173
56
 
174
57
  // Always include the logical ID field - the storage adapter translates it.
175
58
  fieldsToSelect.add('id')
176
59
 
177
- // Handle both Schema objects and plain objects
178
- if (!schemaStructure) {
179
- schemaStructure = schemaInstance?.structure || schemaInstance || {}
180
- }
181
-
182
60
  // Get computed fields and virtual fields from schema
183
61
  const computedFieldNames = new Set(Object.keys(computedFields))
184
62
  const queryFieldNames = new Set(Object.keys(queryFields))
63
+ /** @type {Set<string>} */
185
64
  const queryFieldsToSelect = new Set()
186
65
 
187
66
  // Find fields marked as virtual in the schema
188
- const virtualFieldNames = new Set()
67
+ /** @type {Map<string, StorageFieldDefinition>} */
68
+ const virtualFields = new Map()
189
69
  Object.entries(schemaStructure).forEach(([fieldName, fieldDef]) => {
190
70
  if (fieldDef.virtual === true) {
191
- virtualFieldNames.add(fieldName)
71
+ virtualFields.set(fieldName, fieldDef)
192
72
  }
193
73
  })
194
74
 
195
- const nonDatabaseFields = new Set([...computedFieldNames, ...virtualFieldNames])
75
+ const nonDatabaseFields = new Set([...computedFieldNames, ...virtualFields.keys()])
196
76
 
197
77
  // Parse requested fields
198
- const requested = requestedFields
199
- ? (
200
- typeof requestedFields === 'string'
201
- ? requestedFields.split(',').map(f => f.trim()).filter(f => f)
202
- : requestedFields
203
- )
204
- : null
205
-
206
- if (requested && requested.length > 0) {
78
+ const requested = parseFieldset(requestedFields)
79
+
80
+ if (requested !== null) {
207
81
  // Sparse fieldsets requested - only select specified fields
208
82
  // Example: ?fields[products]=name,price,profit_margin
209
83
  requested.forEach(field => {
210
84
  if (field === 'id' || field === idProperty) return
85
+ if (findRelationshipDefinition(scopeSchemaInfo, field)) return
211
86
 
212
87
  if (queryFieldNames.has(field)) {
213
88
  if (queryFields[field]?.hidden === true) return
@@ -238,43 +113,6 @@ export const buildFieldSelection = async (scope, deps) => {
238
113
 
239
114
  fieldsToSelect.add(field)
240
115
  })
241
-
242
- // Handle computed field dependencies - fetch fields needed for calculations
243
- // Example: User requests 'profit_margin' which depends on 'price' and 'cost'
244
- // We need to fetch price and cost from DB even if not explicitly requested
245
- const requestedComputedFields = requested.filter(f => computedFieldNames.has(f))
246
- for (const computedField of requestedComputedFields) {
247
- const fieldDef = computedFields[computedField]
248
- if (fieldDef.dependencies) {
249
- for (const dep of fieldDef.dependencies) {
250
- const depFieldDef = schemaStructure[dep]
251
- // Only add dependency if it exists and isn't hidden
252
- if (depFieldDef && depFieldDef.hidden !== true) {
253
- fieldsToSelect.add(dep)
254
- // Track dependencies that weren't explicitly requested
255
- // These will be removed from the final response
256
- if (!requested.includes(dep)) {
257
- computedDependencies.add(dep)
258
- }
259
- }
260
- }
261
- }
262
- }
263
-
264
- // Still handle normallyHidden fields for backward compatibility
265
- if (requestedComputedFields.length > 0) {
266
- Object.entries(schemaStructure).forEach(([field, fieldDef]) => {
267
- if (fieldDef.normallyHidden === true && fieldDef.hidden !== true) {
268
- // Only add if not already handled by dependencies
269
- if (!fieldsToSelect.has(field)) {
270
- fieldsToSelect.add(field)
271
- if (!requested.includes(field)) {
272
- computedDependencies.add(field)
273
- }
274
- }
275
- }
276
- })
277
- }
278
116
  } else {
279
117
  // No sparse fieldsets - return all visible fields
280
118
  // This is the default behavior when no ?fields parameter is provided
@@ -298,26 +136,6 @@ export const buildFieldSelection = async (scope, deps) => {
298
136
  fieldsToSelect.add(field)
299
137
  })
300
138
 
301
- // When no sparse fieldsets, we compute all computed fields
302
- // So we need to include their dependencies even if normallyHidden
303
- // Example: profit_margin depends on 'cost' which is normallyHidden
304
- // We fetch 'cost' for calculation but don't return it in response
305
- for (const [, fieldDef] of Object.entries(computedFields)) {
306
- if (fieldDef.dependencies) {
307
- for (const dep of fieldDef.dependencies) {
308
- const depFieldDef = schemaStructure[dep]
309
- if (depFieldDef && depFieldDef.hidden !== true) {
310
- fieldsToSelect.add(dep)
311
- // Track normallyHidden dependencies for later removal
312
- // These are fetched for computation but not returned
313
- if (depFieldDef.normallyHidden === true) {
314
- computedDependencies.add(dep)
315
- }
316
- }
317
- }
318
- }
319
- }
320
-
321
139
  for (const [fieldName, fieldDef] of Object.entries(queryFields)) {
322
140
  if (fieldDef.hidden === true || fieldDef.normallyHidden === true) continue
323
141
  queryFieldsToSelect.add(fieldName)
@@ -330,7 +148,7 @@ export const buildFieldSelection = async (scope, deps) => {
330
148
 
331
149
  for (const sortField of sortFields) {
332
150
  if (typeof sortField !== 'string') continue
333
- const normalizedField = sortField.startsWith('-') ? sortField.slice(1) : sortField
151
+ const normalizedField = resolveSortField(sortField.startsWith('-') ? sortField.slice(1) : sortField, scopeSchemaInfo)
334
152
 
335
153
  if (queryFieldNames.has(normalizedField)) {
336
154
  queryFieldsToSelect.add(normalizedField)
@@ -351,15 +169,35 @@ export const buildFieldSelection = async (scope, deps) => {
351
169
 
352
170
  // Always include polymorphic type and id fields from relationships
353
171
  try {
354
- const relationships = scope.vars.schemaInfo.schemaRelationships
172
+ const relationships = scopeSchemaInfo.schemaRelationships
355
173
  Object.entries(relationships || {}).forEach(([relName, relDef]) => {
356
174
  if (relDef.belongsToPolymorphic) {
357
- if (relDef.typeField) fieldsToSelect.add(relDef.typeField)
358
- if (relDef.idField) fieldsToSelect.add(relDef.idField)
175
+ fieldsToSelect.add(relDef.belongsToPolymorphic.typeField)
176
+ fieldsToSelect.add(relDef.belongsToPolymorphic.idField)
359
177
  }
360
178
  })
361
- } catch (e) {
362
- // 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)
363
201
  }
364
202
 
365
203
  // Return detailed information about field selection
@@ -376,6 +214,7 @@ export const buildFieldSelection = async (scope, deps) => {
376
214
  }
377
215
  }
378
216
 
217
+ /** @param {SelectionQueryRequest} options @returns {Promise<SelectedQuery>} */
379
218
  export const applyFieldSelectionToQuery = async ({
380
219
  query,
381
220
  scope,
@@ -388,18 +227,34 @@ export const applyFieldSelectionToQuery = async ({
388
227
  scopeName = ''
389
228
  }) => {
390
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
+ }
391
246
 
392
247
  const selectedQuery = buildQuerySelection(
393
248
  query,
394
249
  tableName,
395
250
  fieldSelectionInfo?.fieldsToSelect || [],
396
251
  useTablePrefix,
397
- selectTranslator ? { translateColumn: selectTranslator } : undefined
252
+ { translateColumn }
398
253
  )
399
254
 
400
255
  const queryFieldRuntimeByField = await buildQueryFieldRuntimes({
401
256
  queryFieldNames: fieldSelectionInfo?.queryFieldsToSelect || [],
402
- queryFields: scope?.vars?.queryFields || {},
257
+ queryFields: scope?.vars?.schemaInfo?.queryFields || {},
403
258
  schemaInfo: scope?.vars?.schemaInfo || {},
404
259
  tableName,
405
260
  storageAdapter,
@@ -419,72 +274,27 @@ export const applyFieldSelectionToQuery = async ({
419
274
  }
420
275
 
421
276
  /**
422
- * Determines which computed fields to calculate based on sparse fieldsets
423
- *
424
- * @param {string} scopeName - Resource name (e.g., 'products')
425
- * @param {Array<string>|string} requestedFields - Requested fields from query
426
- * @param {Object} computedFields - Computed field definitions from schema
427
- * @returns {Array<string>} Names of computed fields to calculate
428
- *
429
- * @example
430
- * // Input: No sparse fieldsets (calculate all computed fields)
431
- * const computed = {
432
- * full_name: { compute: (r) => `${r.first} ${r.last}` },
433
- * age: { compute: (r) => new Date().getFullYear() - r.birth_year }
434
- * };
435
- * getRequestedComputedFields('users', null, computed);
436
- * // Output: ['full_name', 'age'] (all computed fields)
437
- *
438
- * @example
439
- * // Input: Sparse fieldsets with some computed fields
440
- * const requestedFields = 'first_name,full_name,email';
441
- * const computed = {
442
- * full_name: { compute: (r) => `${r.first} ${r.last}` },
443
- * age: { compute: (r) => new Date().getFullYear() - r.birth_year }
444
- * };
445
- * getRequestedComputedFields('users', requestedFields, computed);
446
- * // Output: ['full_name'] (only requested computed field)
447
- *
448
- * @example
449
- * // Input: Sparse fieldsets with no computed fields
450
- * const requestedFields = ['first_name', 'email'];
451
- * const computed = {
452
- * full_name: { compute: (r) => `${r.first} ${r.last}` }
453
- * };
454
- * getRequestedComputedFields('users', requestedFields, computed);
455
- * // Output: [] (no computed fields requested)
456
- *
457
- * @description
458
- * Used by:
459
- * - enrichAttributes to determine which computations to run
460
- * - Applied after fetching data from database
461
- *
462
- * Purpose:
463
- * - Optimizes performance by only computing requested fields
464
- * - Supports JSON:API sparse fieldsets for computed fields
465
- * - Handles both string and array input formats
466
- *
467
- * Data flow:
468
- * 1. Checks if sparse fieldsets are specified
469
- * 2. If not, returns all computed field names
470
- * 3. If yes, filters to only requested computed fields
471
- * 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[]}
472
283
  */
473
284
  export const getRequestedComputedFields = (scopeName, requestedFields, computedFields) => {
474
285
  if (!computedFields) return []
475
286
 
476
- const allComputedFields = Object.keys(computedFields)
477
-
478
- if (!requestedFields || requestedFields.length === 0) {
479
- // No sparse fieldsets - return all computed fields
480
- 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)
481
292
  }
482
293
 
483
- // Parse requested fields if it's a string
484
- const requested = typeof requestedFields === 'string'
485
- ? requestedFields.split(',').map(f => f.trim()).filter(f => f)
486
- : requestedFields
487
-
488
- // Return only requested computed fields that exist
489
- 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
+ })
490
300
  }