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
@@ -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
  }