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,1252 +0,0 @@
1
- # 2.7 Field Transformations
2
-
3
- The JSON REST API library provides a comprehensive system for transforming data as it flows through your application. This chapter covers all the ways you can transform, compute, and control field visibility in your API.
4
-
5
- ## Overview
6
-
7
- Field transformations allow you to:
8
- - Accept temporary data that isn't stored (virtual fields)
9
- - Transform data before storing it (setters)
10
- - Transform data when retrieving it (getters)
11
- - Calculate values from other fields (computed fields)
12
- - Control which fields are visible in responses (hidden fields)
13
-
14
- For SQL-backed derived values that must participate in sorting or cursor pagination, use [Query Projections](GUIDE_X_Query_Projections.md). Those are query-layer fields, not normal computed fields.
15
-
16
- ## The Data Transformation Pipeline
17
-
18
- Understanding when each transformation occurs is crucial for building robust APIs:
19
-
20
- ```
21
- INPUT FLOW (POST/PUT/PATCH):
22
- ┌─────────────┐ ┌──────────────────┐ ┌─────────┐ ┌──────────┐
23
- │ User Input │ --> │ Validate Schema │ --> │ Setters │ --> │ Database │
24
- │ (JSON) │ │ (including virt) │ │ │ │ (no virt)│
25
- └─────────────┘ └──────────────────┘ └─────────┘ └──────────┘
26
- ↓
27
- Virtual fields validated
28
- and preserved for response
29
-
30
- OUTPUT FLOW (GET):
31
- ┌──────────┐ ┌─────────┐ ┌──────────────┐ ┌─────────────────┐ ┌──────────┐
32
- │ Database │ --> │ Getters │ --> │ Computed │ --> │ Merge Virtual & │ --> │ Response │
33
- │ │ │ │ │ Fields │ │ Apply Hidden │ │ (JSON) │
34
- └──────────┘ └─────────┘ └──────────────┘ └─────────────────┘ └──────────┘
35
- ```
36
-
37
- ## Virtual Fields
38
-
39
- Virtual fields are fields that pass through the API but are never stored in the database. They're perfect for temporary data needed during request processing.
40
-
41
- ### Defining Virtual Fields
42
-
43
- ```javascript
44
- await api.addResource('users', {
45
- schema: {
46
- username: { type: 'string', required: true },
47
- email: { type: 'string', required: true },
48
- password: { type: 'string', required: true, hidden: true },
49
-
50
- // Virtual fields - not stored in database
51
- passwordConfirmation: { type: 'string', virtual: true },
52
- termsAccepted: { type: 'boolean', virtual: true },
53
- captchaToken: { type: 'string', virtual: true }
54
- }
55
- });
56
- ```
57
-
58
- ### Common Use Cases
59
-
60
- 1. **Password Confirmation**
61
- ```javascript
62
- // Client sends:
63
- {
64
- "username": "john",
65
- "password": "secret123",
66
- "passwordConfirmation": "secret123" // Virtual field
67
- }
68
-
69
- // Use in a hook to validate:
70
- api.on('beforeData:create:users', ({ inputRecord }) => {
71
- const { password, passwordConfirmation } = inputRecord.data.attributes;
72
- if (password !== passwordConfirmation) {
73
- throw new Error('Passwords do not match');
74
- }
75
- });
76
- ```
77
-
78
- 2. **Terms Acceptance**
79
- ```javascript
80
- api.on('beforeData:create:users', ({ inputRecord }) => {
81
- if (!inputRecord.data.attributes.termsAccepted) {
82
- throw new Error('You must accept the terms of service');
83
- }
84
- });
85
- ```
86
-
87
- 3. **UI State or Metadata**
88
- ```javascript
89
- // Client can send UI state that's returned but not stored
90
- {
91
- "title": "My Article",
92
- "content": "...",
93
- "editorState": { ... }, // Virtual field with editor metadata
94
- "isDraft": true // Virtual field for UI state
95
- }
96
- ```
97
-
98
- ### Key Characteristics
99
-
100
- - **Input**: Accepted in POST/PUT/PATCH requests
101
- - **Validation**: Validated according to schema rules
102
- - **Storage**: Never stored in the database
103
- - **Output**: Returned in responses if provided
104
- - **Hooks**: Available to all hooks during request processing
105
-
106
- ## Setters and Getters
107
-
108
- Setters and getters transform data at the database boundary. Setters run before saving, getters run after loading.
109
-
110
- ### Setters - Transform Before Storage
111
-
112
- ```javascript
113
- await api.addResource('users', {
114
- schema: {
115
- email: {
116
- type: 'string',
117
- required: true,
118
- setter: (value) => value.toLowerCase().trim()
119
- },
120
- phone: {
121
- type: 'string',
122
- setter: (value) => {
123
- // Remove all non-digits
124
- return value ? value.replace(/\D/g, '') : null;
125
- }
126
- },
127
- metadata: {
128
- type: 'object',
129
- setter: (value) => JSON.stringify(value || {})
130
- }
131
- }
132
- });
133
- ```
134
-
135
- If a setter throws or rejects, the write fails and the transaction rolls back. Typed API errors retain their code and details; other setter failures become validation errors. Getters and computed fields also propagate typed API errors, including `RestApiTemporalDataError`, so invalid output cannot be silently accepted.
136
-
137
- ### Getters - Transform After Retrieval
138
-
139
- ```javascript
140
- await api.addResource('users', {
141
- schema: {
142
- email: {
143
- type: 'string',
144
- getter: (value) => value?.toLowerCase()
145
- },
146
- phone: {
147
- type: 'string',
148
- getter: (value) => {
149
- // Format as (XXX) XXX-XXXX
150
- if (!value || value.length !== 10) return value;
151
- return `(${value.slice(0,3)}) ${value.slice(3,6)}-${value.slice(6)}`;
152
- }
153
- },
154
- metadata: {
155
- type: 'string',
156
- getter: (value) => {
157
- try {
158
- return value ? JSON.parse(value) : {};
159
- } catch {
160
- return {};
161
- }
162
- }
163
- }
164
- }
165
- });
166
- ```
167
-
168
- ### Using Virtual Fields with Setters
169
-
170
- A common pattern is using virtual fields to provide data that setters will process:
171
-
172
- ```javascript
173
- await api.addResource('products', {
174
- schema: {
175
- price: {
176
- type: 'number',
177
- setter: function(value, { attributes }) {
178
- // Use virtual priceInCents if provided
179
- if (attributes.priceInCents !== undefined) {
180
- return attributes.priceInCents / 100;
181
- }
182
- return value;
183
- }
184
- },
185
- priceInCents: { type: 'number', virtual: true }
186
- }
187
- });
188
-
189
- // Client can send either format:
190
- // { "price": 19.99 }
191
- // OR
192
- // { "priceInCents": 1999 }
193
- ```
194
-
195
- ### Async Setters and Getters
196
-
197
- Both setters and getters can be async:
198
-
199
- ```javascript
200
- await api.addResource('secure_data', {
201
- schema: {
202
- secret: {
203
- type: 'string',
204
- setter: async (value) => {
205
- // Encrypt before storing
206
- const encrypted = await encrypt(value);
207
- return encrypted;
208
- },
209
- getter: async (value) => {
210
- // Decrypt after retrieving
211
- const decrypted = await decrypt(value);
212
- return decrypted;
213
- }
214
- }
215
- }
216
- });
217
- ```
218
-
219
- ### Setter and Getter Context
220
-
221
- Both functions receive a context object as the second parameter:
222
-
223
- ```javascript
224
- setter: (value, context) => {
225
- // context contains:
226
- // - attributes: all field values
227
- // - record: the full record (on updates)
228
- // - scopeName: resource name
229
- // - method: 'post', 'put', or 'patch'
230
-
231
- if (context.method === 'post') {
232
- // Special handling for creation
233
- }
234
- return value;
235
- }
236
- ```
237
-
238
- ## Computed Fields
239
-
240
- Computed fields are output-only fields calculated from other fields. They're never stored and always calculated fresh when requested.
241
-
242
- If a derived field must participate in SQL list behavior such as sorting or cursor pagination, use a query projection instead. Query projections are documented separately in [Query Projections](GUIDE_X_Query_Projections.md).
243
-
244
- ### Basic Computed Fields
245
-
246
- ```javascript
247
- await api.addResource('products', {
248
- schema: {
249
- name: { type: 'string', required: true },
250
- price: { type: 'number', required: true },
251
- cost: { type: 'number', required: true, normallyHidden: true },
252
-
253
- // Computed fields
254
- profitMargin: {
255
- type: 'number',
256
- computed: true,
257
- dependencies: ['price', 'cost'],
258
- compute: ({ attributes }) => {
259
- if (!attributes.price || !attributes.cost) return null;
260
- return ((attributes.price - attributes.cost) / attributes.price * 100).toFixed(2);
261
- }
262
- },
263
-
264
- displayName: {
265
- type: 'string',
266
- computed: true,
267
- dependencies: ['name', 'price'],
268
- compute: ({ attributes }) => {
269
- return `${attributes.name} - $${attributes.price}`;
270
- }
271
- }
272
- }
273
- });
274
- ```
275
-
276
- ### Dependencies and Hidden Fields
277
-
278
- Computed fields can depend on hidden fields:
279
-
280
- ```javascript
281
- await api.addResource('users', {
282
- schema: {
283
- firstName: { type: 'string' },
284
- lastName: { type: 'string' },
285
- dateOfBirth: { type: 'date', normallyHidden: true },
286
-
287
- fullName: {
288
- type: 'string',
289
- computed: true,
290
- dependencies: ['firstName', 'lastName'],
291
- compute: ({ attributes }) => {
292
- return [attributes.firstName, attributes.lastName]
293
- .filter(Boolean)
294
- .join(' ');
295
- }
296
- },
297
-
298
- age: {
299
- type: 'number',
300
- computed: true,
301
- dependencies: ['dateOfBirth'], // Depends on hidden field
302
- compute: ({ attributes }) => {
303
- if (!attributes.dateOfBirth) return null;
304
- const birth = new Date(attributes.dateOfBirth);
305
- const today = new Date();
306
- let age = today.getFullYear() - birth.getFullYear();
307
- const monthDiff = today.getMonth() - birth.getMonth();
308
- if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
309
- age--;
310
- }
311
- return age;
312
- }
313
- }
314
- }
315
- });
316
- ```
317
-
318
- ### Async Computed Fields
319
-
320
- ```javascript
321
- await api.addResource('products', {
322
- schema: {
323
- id: { type: 'id' },
324
- name: { type: 'string' },
325
-
326
- inventoryStatus: {
327
- type: 'string',
328
- computed: true,
329
- dependencies: ['id'],
330
- compute: async ({ attributes, context }) => {
331
- // Could check external inventory system
332
- const count = await context.knex('inventory')
333
- .where('product_id', attributes.id)
334
- .sum('quantity as total')
335
- .first();
336
-
337
- if (count.total > 100) return 'In Stock';
338
- if (count.total > 0) return 'Low Stock';
339
- return 'Out of Stock';
340
- }
341
- }
342
- }
343
- });
344
- ```
345
-
346
- ### Compute Function Context
347
-
348
- The compute function receives a rich context:
349
-
350
- ```javascript
351
- compute: (context) => {
352
- // context contains:
353
- // - attributes: all record attributes (including dependencies)
354
- // - record: full record with id
355
- // - context: request context with knex, transaction, etc.
356
- // - helpers: API helpers
357
- // - api: full API instance
358
- // - scopeName: current resource name
359
- }
360
- ```
361
-
362
- ### Important Notes on Computed Fields
363
-
364
- 1. **Output Only**: If a computed field is sent in input, it's ignored with a warning
365
- 2. **Always Fresh**: Calculated on every request (no caching)
366
- 3. **Dependencies**: The system automatically fetches dependency fields from the database
367
- 4. **Sparse Fieldsets**: Work seamlessly with JSON:API sparse fieldsets
368
- 5. **Performance**: Keep computations fast as they run on every request
369
-
370
- ## Hidden Fields
371
-
372
- Control which fields are visible in API responses:
373
-
374
- ### Hidden Fields - Never Visible
375
-
376
- ```javascript
377
- await api.addResource('users', {
378
- schema: {
379
- email: { type: 'string', required: true },
380
- passwordHash: { type: 'string', hidden: true },
381
- salt: { type: 'string', hidden: true },
382
- internalNotes: { type: 'string', hidden: true }
383
- }
384
- });
385
-
386
- // These fields are NEVER returned in responses, even if explicitly requested
387
- ```
388
-
389
- ### Normally Hidden Fields - Available on Request
390
-
391
- ```javascript
392
- await api.addResource('products', {
393
- schema: {
394
- name: { type: 'string', required: true },
395
- price: { type: 'number', required: true },
396
- cost: { type: 'number', normallyHidden: true },
397
- supplierCode: { type: 'string', normallyHidden: true }
398
- }
399
- });
400
-
401
- // Hidden by default
402
- GET /products/1
403
- // Returns: { id: '1', name: 'Widget', price: 99.99 }
404
-
405
- // Explicitly request hidden fields
406
- GET /products/1?fields[products]=name,price,cost
407
- // Returns: { id: '1', name: 'Widget', price: 99.99, cost: 45.00 }
408
- ```
409
-
410
- ## Advanced Transformations with Hooks
411
-
412
- For complex transformations that depend on context (user permissions, time of day, etc.), use the `enrichAttributes` hook:
413
-
414
- ```javascript
415
- // Add permission-based field visibility
416
- api.on('enrichAttributes', ({ attributes, context }) => {
417
- if (context.user?.role !== 'admin') {
418
- delete attributes.profitMargin;
419
- delete attributes.cost;
420
- }
421
- return attributes;
422
- });
423
-
424
- // Add dynamic computed fields
425
- api.on('enrichAttributes', ({ attributes, context }) => {
426
- if (context.scopeName === 'products') {
427
- attributes.isOnSale = attributes.price < attributes.regularPrice;
428
- }
429
- return attributes;
430
- });
431
- ```
432
-
433
- For more details on hooks, see the [Hooks Documentation](./GUIDE_3_Hooks.md).
434
-
435
- ## Complete Example: E-commerce Product
436
-
437
- Here's a complete example showing all transformation types working together:
438
-
439
- ```javascript
440
- await api.addResource('products', {
441
- schema: {
442
- // Regular fields
443
- sku: { type: 'string', required: true },
444
- name: { type: 'string', required: true },
445
- description: { type: 'string' },
446
-
447
- // Price with setter for cents conversion
448
- price: {
449
- type: 'number',
450
- required: true,
451
- setter: function(value, { attributes }) {
452
- // Accept price in cents via virtual field
453
- if (attributes.priceInCents !== undefined) {
454
- return attributes.priceInCents / 100;
455
- }
456
- return value;
457
- },
458
- getter: (value) => Number(value.toFixed(2))
459
- },
460
-
461
- // Hidden cost field
462
- cost: {
463
- type: 'number',
464
- required: true,
465
- normallyHidden: true
466
- },
467
-
468
- // Never visible
469
- supplierApiKey: {
470
- type: 'string',
471
- hidden: true
472
- },
473
-
474
- // Virtual fields for input
475
- priceInCents: { type: 'number', virtual: true },
476
- importFromSupplier: { type: 'boolean', virtual: true },
477
-
478
- // Computed fields for output
479
- profitMargin: {
480
- type: 'number',
481
- computed: true,
482
- dependencies: ['price', 'cost'],
483
- compute: ({ attributes }) => {
484
- if (!attributes.price || !attributes.cost) return null;
485
- return ((attributes.price - attributes.cost) / attributes.price * 100).toFixed(2);
486
- }
487
- },
488
-
489
- displayPrice: {
490
- type: 'string',
491
- computed: true,
492
- dependencies: ['price'],
493
- compute: ({ attributes }) => {
494
- return `$${attributes.price.toFixed(2)}`;
495
- }
496
- }
497
- }
498
- });
499
-
500
- // Usage example:
501
- // POST /products
502
- {
503
- "data": {
504
- "type": "products",
505
- "attributes": {
506
- "sku": "WIDGET-001",
507
- "name": "Super Widget",
508
- "description": "The best widget",
509
- "priceInCents": 9999, // Virtual field (converted to 99.99)
510
- "cost": 45.00,
511
- "importFromSupplier": true, // Virtual field (triggers hook)
512
- "supplierApiKey": "secret" // Hidden field (stored but never returned)
513
- }
514
- }
515
- }
516
-
517
- // Hook to handle virtual field
518
- api.on('beforeData:create:products', async ({ inputRecord, context }) => {
519
- if (inputRecord.data.attributes.importFromSupplier) {
520
- // Use the hidden supplierApiKey to fetch data
521
- const data = await fetchFromSupplier(inputRecord.data.attributes.supplierApiKey);
522
- inputRecord.data.attributes.description = data.description;
523
- }
524
- });
525
-
526
- // Response:
527
- {
528
- "data": {
529
- "type": "products",
530
- "id": "1",
531
- "attributes": {
532
- "sku": "WIDGET-001",
533
- "name": "Super Widget",
534
- "description": "The best widget",
535
- "price": 99.99, // Setter converted from cents
536
- "displayPrice": "$99.99", // Computed field
537
- "profitMargin": "54.95", // Computed field (if user has permission)
538
- "importFromSupplier": true // Virtual field preserved
539
- // Note: cost, supplierApiKey not included
540
- }
541
- }
542
- }
543
- ```
544
-
545
- ## Best Practices
546
-
547
- ### When to Use Each Transformation Type
548
-
549
- | Need | Use | Example |
550
- |------|-----|---------|
551
- | Temporary request data | Virtual field | Password confirmation |
552
- | Clean input data | Setter | Lowercase emails, trim whitespace |
553
- | Format output data | Getter | Format phone numbers, parse JSON |
554
- | Calculate from other fields | Computed field | Full names, totals, percentages |
555
- | Security-sensitive data | Hidden field | Password hashes, API keys |
556
- | Sensitive business data | Normally hidden field | Costs, internal notes |
557
- | Context-aware transforms | enrichAttributes hook | Permission-based visibility |
558
-
559
- ### Performance Considerations
560
-
561
- 1. **Setters/Getters**: Keep them fast and synchronous when possible
562
- 2. **Computed Fields**:
563
- - Calculated on every request (no caching)
564
- - Dependencies are always fetched from DB
565
- - Consider storing frequently accessed computed values
566
- 3. **Virtual Fields**: No performance impact (not stored/retrieved)
567
- 4. **Hidden Fields**: Filtered after retrieval (minimal impact)
568
-
569
- ### Common Pitfalls to Avoid
570
-
571
- 1. **Don't use computed fields for heavy calculations** - Consider background jobs instead
572
- 2. **Don't put validation logic in setters** - Use schema validation or hooks
573
- 3. **Remember computed fields are output-only** - They're ignored in input
574
- 4. **Test edge cases** - Null values, missing dependencies, etc.
575
- 5. **Document virtual fields** - They're part of your API contract
576
-
577
- ## Migration Tips
578
-
579
- If you're migrating from an older version:
580
- - Computed fields now use `computed: true` in the schema (not a separate object)
581
- - Virtual fields use `virtual: true` in the schema
582
- - All field transformations are defined in one place: the schema
583
-
584
- ## Summary
585
-
586
- The JSON REST API library provides a complete transformation pipeline:
587
- - **Virtual fields** for temporary data that flows through but isn't stored
588
- - **Setters** for cleaning and transforming input before storage
589
- - **Getters** for formatting and transforming output after retrieval
590
- - **Computed fields** for deriving values from other fields
591
- - **Hidden fields** for controlling visibility
592
- - **Hooks** for advanced context-aware transformations
593
-
594
- By combining these tools, you can build APIs that accept user-friendly input, store data efficiently, and return perfectly formatted responses.
595
-
596
- ---
597
-
598
- # Detailed Guide: Computed Fields
599
-
600
- Computed fields are virtual fields that don't exist in your database but are calculated on-the-fly from other fields. They're computed after the database load, every time they're requested, and can depend on other fields (including hidden ones). Computed fields work seamlessly with sparse fieldsets and are calculated for both main resources and included resources.
601
-
602
- Let's create a complete example with products and reviews:
603
-
604
- ```javascript
605
- // Define products resource with computed fields
606
- await api.addResource('products', {
607
- schema: {
608
- name: { type: 'string', required: true, max: 255 },
609
- price: { type: 'number', required: true, min: 0 },
610
- cost: { type: 'number', required: true, min: 0, normallyHidden: true },
611
- profit_margin: {
612
- type: 'number',
613
- computed: true, // Mark as computed field
614
- dependencies: ['price', 'cost'],
615
- compute: ({ attributes }) => {
616
- if (!attributes.price || attributes.price === 0) return 0;
617
- return ((attributes.price - attributes.cost) / attributes.price * 100).toFixed(2);
618
- }
619
- },
620
- // Computed fields can also be async
621
- availability_status: {
622
- type: 'string',
623
- computed: true, // Mark as computed field
624
- dependencies: ['name'],
625
- compute: async ({ attributes }) => {
626
- // Simulate async operation (e.g., checking external inventory)
627
- await new Promise(resolve => setTimeout(resolve, 10));
628
- return `${attributes.name} - In Stock`;
629
- }
630
- }
631
- },
632
- relationships: {
633
- reviews: { type: 'hasMany', target: 'reviews', foreignKey: 'product_id' }
634
- }
635
- });
636
- await api.resources.products.createKnexTable();
637
-
638
- // Define reviews resource with computed fields
639
- await api.addResource('reviews', {
640
- schema: {
641
- product_id: { type: 'id', belongsTo: 'products', as: 'product', required: true },
642
- reviewer_name: { type: 'string', required: true },
643
- rating: { type: 'number', required: true, min: 1, max: 5 },
644
- comment: { type: 'string', max: 1000 },
645
- helpful_votes: { type: 'number', default: 0 },
646
- total_votes: { type: 'number', default: 0 },
647
- helpfulness_score: {
648
- type: 'number',
649
- computed: true, // Mark as computed field
650
- dependencies: ['helpful_votes', 'total_votes'],
651
- compute: ({ attributes }) => {
652
- if (attributes.total_votes === 0) return null;
653
- return ((attributes.helpful_votes / attributes.total_votes) * 100).toFixed(0);
654
- }
655
- }
656
- }
657
- });
658
- await api.resources.reviews.createKnexTable();
659
- ```
660
-
661
- The key features to note:
662
- - `cost` is marked as `normallyHidden` - it won't be returned unless explicitly requested
663
- - `profit_margin` depends on both `price` and `cost`
664
- - `helpfulness_score` is computed for each review
665
-
666
- ## Basic Usage
667
-
668
- Let's create some data and see how computed fields work:
669
-
670
- ```javascript
671
- // Create a product
672
- const product = await api.resources.products.post({
673
- name: 'Premium Headphones',
674
- price: 199.99,
675
- cost: 89.50
676
- });
677
-
678
- // Fetch the product - computed fields are automatically calculated
679
- const fetchedProduct = await api.resources.products.get({ id: product.id });
680
- console.log(fetchedProduct);
681
- // {
682
- // id: '1',
683
- // name: 'Premium Headphones',
684
- // price: 199.99,
685
- // profit_margin: '55.23', // Computed: (199.99 - 89.50) / 199.99 * 100
686
- // availability_status: 'Premium Headphones - In Stock' // Async computed field
687
- // }
688
- // Note: 'cost' is not included (normallyHidden)
689
-
690
- // Add some reviews
691
- const review1 = await api.resources.reviews.post({
692
- product_id: product.id,
693
- reviewer_name: 'Alice',
694
- rating: 5,
695
- comment: 'Excellent sound quality!',
696
- helpful_votes: 45,
697
- total_votes: 50
698
- });
699
- console.log(review1);
700
-
701
- const review2 = await api.resources.reviews.post({
702
- product_id: product.id,
703
- reviewer_name: 'Bob',
704
- rating: 4,
705
- comment: 'Good, but a bit pricey',
706
- helpful_votes: 10,
707
- total_votes: 25
708
- });
709
- console.log(review2);
710
-
711
-
712
- ```
713
-
714
- ## Sparse Fieldsets and Dependencies
715
-
716
- When you request a computed field via sparse fieldsets, the system automatically fetches its dependencies:
717
-
718
- ```javascript
719
- // Request only name and profit_margin
720
- const sparseProduct = await api.resources.products.get({
721
- id: product.id,
722
- queryParams: {
723
- fields: { products: 'name,profit_margin' }
724
- }
725
- });
726
- console.log('Product with sparse fields:', sparseProduct);
727
-
728
- const productWithCost = await api.resources.products.get({
729
- id: product.id,
730
- queryParams: {
731
- fields: { products: 'name,cost,profit_margin' }
732
- }
733
- });
734
- console.log('Product with sparse fields includig cost:', productWithCost);
735
- ```
736
-
737
- **Expected output**:
738
-
739
- ```text
740
- Product with sparse fields: {
741
- id: '1',
742
- name: 'Premium Headphones',
743
- profit_margin: '55.25',
744
- availability_status: 'Premium Headphones - In Stock',
745
- reviews_ids: [ '1', '2' ]
746
- }
747
- Product with sparse fields includig cost: {
748
- id: '1',
749
- name: 'Premium Headphones',
750
- cost: 89.5,
751
- profit_margin: '55.25',
752
- reviews_ids: [ '1', '2' ]
753
- }
754
- ```
755
-
756
- ## Computed Fields in Included Resources
757
-
758
- Computed fields work seamlessly with included resources:
759
-
760
- ```javascript
761
- // Fetch product with reviews
762
- const productWithReviews = await api.resources.products.get({
763
- id: product.id,
764
- queryParams: {
765
- include: ['reviews']
766
- }
767
- });
768
- console.log('Product With Reviews:', productWithReviews);
769
-
770
- // Use sparse fieldsets on included resources
771
- const productWithSparseReviews = await api.resources.products.get({
772
- id: product.id,
773
- queryParams: {
774
- include: ['reviews'],
775
- fields: {
776
- products: 'name,price',
777
- reviews: 'reviewer_name,rating,helpfulness_score' // Only these fields
778
- }
779
- }
780
- });
781
- console.log('Product With Sparse Reviews:', productWithSparseReviews);
782
- ```
783
-
784
- **Expected Output**:
785
-
786
- ```text
787
- Product With Reviews: {
788
- id: '1',
789
- name: 'Premium Headphones',
790
- price: 199.99,
791
- profit_margin: '55.25',
792
- availability_status: 'Premium Headphones - In Stock',
793
- reviews_ids: [ '1', '2' ],
794
- reviews: [
795
- {
796
- id: '1',
797
- reviewer_name: 'Alice',
798
- rating: 5,
799
- comment: 'Excellent sound quality!',
800
- helpful_votes: 45,
801
- total_votes: 50,
802
- helpfulness_score: '90'
803
- },
804
- {
805
- id: '2',
806
- reviewer_name: 'Bob',
807
- rating: 4,
808
- comment: 'Good, but a bit pricey',
809
- helpful_votes: 10,
810
- total_votes: 25,
811
- helpfulness_score: '40'
812
- }
813
- ]
814
- }
815
- Product With Sparse Reviews: {
816
- id: '1',
817
- name: 'Premium Headphones',
818
- price: 199.99,
819
- reviews_ids: [ '1', '2' ],
820
- reviews: [
821
- {
822
- id: '1',
823
- reviewer_name: 'Alice',
824
- rating: 5,
825
- helpfulness_score: '90'
826
- },
827
- {
828
- id: '2',
829
- reviewer_name: 'Bob',
830
- rating: 4,
831
- helpfulness_score: '40'
832
- }
833
- ]
834
- }
835
- ```
836
-
837
- ## Error Handling
838
-
839
- Computed fields handle errors gracefully:
840
-
841
- ```javascript
842
-
843
- // Create a review with no votes
844
- const review3 = await api.resources.reviews.post({
845
- product_id: product.id,
846
- reviewer_name: 'Charlie',
847
- rating: 3,
848
- comment: 'Average product',
849
- helpful_votes: 0,
850
- total_votes: 0 // This will cause division by zero
851
- });
852
-
853
- const fetchedReviewWithError = await api.resources.reviews.get({ id: review3.id });
854
- console.log('Fetched review (with error in helpfulness score):', fetchedReviewWithError
855
- ```
856
-
857
- **Expected Output**:
858
-
859
- ```text
860
- Fetched review (with error in helpfulness score): {
861
- id: '3',
862
- reviewer_name: 'Charlie',
863
- rating: 3,
864
- comment: 'Average product',
865
- helpful_votes: 0,
866
- total_votes: 0,
867
- helpfulness_score: null,
868
- product_id: '1'
869
- }
870
- ```
871
-
872
- ## Key Points
873
-
874
- 1. **Always Computed** - Computed fields are calculated fresh on every request, there's no caching.
875
-
876
- 2. **Dependencies Are Fetched** - When you request a computed field, all its dependencies are automatically fetched from the database, even if they won't appear in the response.
877
-
878
- 3. **Works with Sparse Fieldsets** - You can request computed fields just like regular fields using sparse fieldsets.
879
-
880
- 4. **Hidden Dependencies** - Fields marked as `normallyHidden` can be used as dependencies and will be fetched for computation, but won't appear in the response unless explicitly requested.
881
-
882
- 5. **Included Resources** - Computed fields are calculated for all resources, whether they're the main resource or included via relationships.
883
-
884
- 6. **Error Handling** - If a computation fails, the field is set to `null` and an error is logged, but the request continues.
885
-
886
- ## Async Computed Fields
887
-
888
- Computed fields can be asynchronous - simply return a Promise or use async/await. The compute function will be awaited during field resolution:
889
-
890
- ```javascript
891
- schema: {
892
- name: { type: 'string', required: true },
893
- availability_status: {
894
- type: 'string',
895
- computed: true, // Mark as computed field
896
- dependencies: ['name'],
897
- compute: async ({ attributes }) => {
898
- // Perform async operation
899
- await new Promise(resolve => setTimeout(resolve, 10));
900
- return `${attributes.name} - In Stock`;
901
- }
902
- }
903
- }
904
- ```
905
-
906
- ## Best Practices
907
-
908
- 1. **Keep Computations Simple** - Computed fields should be quick calculations. While async is supported, avoid heavy operations like database queries or external API calls.
909
-
910
- 2. **Declare All Dependencies** - Always list all fields your computation needs in the `dependencies` array.
911
-
912
- 3. **Handle Edge Cases** - Check for null values and division by zero in your compute functions.
913
-
914
- 4. **Consider Performance** - Remember that dependencies are always fetched. If you have expensive computations or many dependencies, consider storing the computed value as a regular field instead.
915
-
916
- ---
917
-
918
- # Detailed Guide: Getters and Setters
919
-
920
- ## Introduction
921
-
922
- Field getters and setters allow you to transform data as it moves between your API and database:
923
-
924
- - **Getters**: Transform data when reading from the database (e.g., formatting phone numbers, trimming strings)
925
- - **Setters**: Transform data before writing to the database (e.g., normalizing emails, hashing passwords)
926
-
927
- This is different from computed fields, which are virtual fields calculated on-the-fly. Getters and setters work with actual database columns.
928
-
929
- ## Initial Setup: A Blog System
930
-
931
- Let's start with a simple blog system without any getters or setters:
932
-
933
- ```javascript
934
- import { Api } from 'hooked-api';
935
- import RestApiPlugin from 'json-rest-api/plugins/core/rest-api-plugin.js';
936
- import RestApiKnexPlugin from 'json-rest-api/plugins/core/rest-api-knex-plugin.js';
937
- import knex from 'knex';
938
-
939
- // Create database connection
940
- const db = knex({
941
- client: 'better-sqlite3',
942
- connection: { filename: './blog.db' }
943
- });
944
-
945
- // Create API instance
946
- const api = new Api({ name: 'blog-api' });
947
-
948
- // Install plugins
949
- await api.use(RestApiPlugin);
950
- await api.use(RestApiKnexPlugin, { knex: db });
951
-
952
- // Define authors resource (no getters/setters yet)
953
- await api.addResource('authors', {
954
- schema: {
955
- id: { type: 'id' },
956
- email: { type: 'string', required: true },
957
- name: { type: 'string', required: true },
958
- bio: { type: 'string', nullable: true }
959
- },
960
- relationships: {
961
- posts: { type: 'hasMany', target: 'posts', foreignKey: 'author_id' }
962
- },
963
- tableName: 'authors'
964
- });
965
-
966
- // Define posts resource (no getters/setters yet)
967
- await api.addResource('posts', {
968
- schema: {
969
- id: { type: 'id' },
970
- title: { type: 'string', required: true },
971
- content: { type: 'string', required: true },
972
- author_id: { type: 'number', belongsTo: 'authors', as: 'author' },
973
- published_at: { type: 'dateTime', temporalPrecision: 3, defaultTo: () => new Date().toISOString() }
974
- },
975
- tableName: 'posts'
976
- });
977
-
978
- // Create tables
979
- await api.resources.authors.createKnexTable();
980
- await api.resources.posts.createKnexTable();
981
-
982
- // Create test data
983
- const author = await api.resources.authors.post({
984
- email: ' Jane.Doe@BLOG.COM ',
985
- name: ' Jane Doe ',
986
- bio: ' Software developer and writer '
987
- });
988
-
989
- const post1 = await api.resources.posts.post({
990
- title: ' Getting Started with APIs ',
991
- content: ' This is my first post about APIs... ',
992
- author_id: author.id
993
- });
994
-
995
- const post2 = await api.resources.posts.post({
996
- title: ' advanced api patterns ',
997
- content: ' Let\'s explore some advanced patterns... ',
998
- author_id: author.id
999
- });
1000
-
1001
- // Fetch author with posts
1002
- const authorWithPosts = await api.resources.authors.get({
1003
- id: author.id,
1004
- queryParams: { include: ['posts'] }
1005
- });
1006
-
1007
- console.log('Author with posts (no getters):', authorWithPosts);
1008
- // Notice the messy data:
1009
- // - Email has spaces and mixed case
1010
- // - Name and bio have extra spaces
1011
- // - Post titles are inconsistently cased
1012
- // - Content has leading/trailing spaces
1013
- ```
1014
-
1015
- ## Adding Getters: Transform Data on Read
1016
-
1017
- Now let's add getters to clean up the data automatically:
1018
-
1019
- ```javascript
1020
- // Enhanced authors resource with getters
1021
- await api.addResource('authors', {
1022
- schema: {
1023
- id: { type: 'id' },
1024
- email: {
1025
- type: 'string',
1026
- required: true,
1027
- getter: (value) => value?.toLowerCase().trim()
1028
- },
1029
- name: {
1030
- type: 'string',
1031
- required: true,
1032
- getter: (value) => value?.trim()
1033
- },
1034
- bio: {
1035
- type: 'string',
1036
- nullable: true,
1037
- getter: (value) => value?.trim()
1038
- }
1039
- },
1040
- relationships: {
1041
- posts: { type: 'hasMany', target: 'posts', foreignKey: 'author_id' }
1042
- },
1043
- tableName: 'authors'
1044
- });
1045
-
1046
- // Enhanced posts resource with getters
1047
- await api.addResource('posts', {
1048
- schema: {
1049
- id: { type: 'id' },
1050
- title: {
1051
- type: 'string',
1052
- required: true,
1053
- getter: (value) => {
1054
- // Capitalize first letter of each word
1055
- return value?.trim()
1056
- .split(' ')
1057
- .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
1058
- .join(' ');
1059
- }
1060
- },
1061
- content: {
1062
- type: 'string',
1063
- required: true,
1064
- getter: (value) => value?.trim()
1065
- },
1066
- author_id: { type: 'number', belongsTo: 'authors', as: 'author' },
1067
- published_at: { type: 'dateTime', temporalPrecision: 3, defaultTo: () => new Date().toISOString() }
1068
- },
1069
- tableName: 'posts'
1070
- });
1071
-
1072
- // Now fetch the same author with posts
1073
- const cleanAuthorWithPosts = await api.resources.authors.get({
1074
- id: author.id,
1075
- queryParams: { include: ['posts'] }
1076
- });
1077
-
1078
- console.log('Author:', cleanAuthorWithPosts);
1079
- // {
1080
- // id: '1',
1081
- // email: 'jane.doe@blog.com', // Normalized
1082
- // name: 'Jane Doe', // Trimmed
1083
- // bio: 'Software developer and writer', // Trimmed
1084
- // posts: [
1085
- // {
1086
- // id: '1',
1087
- // title: 'Getting Started With Apis', // Title case
1088
- // content: 'This is my first post about APIs...', // Trimmed
1089
- // author_id: 1,
1090
- // published_at: '2024-01-15T10:30:00.000Z'
1091
- // },
1092
- // {
1093
- // id: '2',
1094
- // title: 'Advanced Api Patterns', // Title case
1095
- // content: 'Let\'s explore some advanced patterns...', // Trimmed
1096
- // author_id: 1,
1097
- // published_at: '2024-01-15T10:31:00.000Z'
1098
- // }
1099
- // ]
1100
- // }
1101
-
1102
- // Getters also work in queries
1103
- const allPosts = await api.resources.posts.query({
1104
- queryParams: {
1105
- include: ['author'],
1106
- filters: { author_id: author.id }
1107
- }
1108
- });
1109
-
1110
- console.log('All posts with author:', allPosts);
1111
- // Both posts and included authors have getters applied
1112
- ```
1113
-
1114
- ## Adding Setters: Transform Data on Write
1115
-
1116
- Setters ensure data is normalized before it's stored:
1117
-
1118
- ```javascript
1119
- // Add setters to authors
1120
- await api.addResource('authors', {
1121
- schema: {
1122
- id: { type: 'id' },
1123
- email: {
1124
- type: 'string',
1125
- required: true,
1126
- setter: (value) => value?.toLowerCase().trim(),
1127
- getter: (value) => value
1128
- },
1129
- name: {
1130
- type: 'string',
1131
- required: true,
1132
- setter: (value) => value?.trim(),
1133
- getter: (value) => value
1134
- },
1135
- bio: {
1136
- type: 'string',
1137
- nullable: true,
1138
- setter: (value) => value?.trim(),
1139
- getter: (value) => value
1140
- }
1141
- },
1142
- relationships: {
1143
- posts: { type: 'hasMany', target: 'posts', foreignKey: 'author_id' }
1144
- },
1145
- tableName: 'authors'
1146
- });
1147
-
1148
- // Now when we create an author, data is cleaned before storage
1149
- const newAuthor = await api.resources.authors.post({
1150
- email: ' JOHN.SMITH@BLOG.COM ',
1151
- name: ' John Smith ',
1152
- bio: ' Tech enthusiast '
1153
- });
1154
-
1155
- console.log('New author:', newAuthor);
1156
- // Data is already clean:
1157
- // {
1158
- // id: '2',
1159
- // email: 'john.smith@blog.com',
1160
- // name: 'John Smith',
1161
- // bio: 'Tech enthusiast'
1162
- // }
1163
- ```
1164
-
1165
- ## Async Setters for Secure Data
1166
-
1167
- Use async setters for operations like password hashing:
1168
-
1169
- ```javascript
1170
- await api.addResource('users', {
1171
- schema: {
1172
- id: { type: 'id' },
1173
- email: {
1174
- type: 'string',
1175
- required: true,
1176
- setter: (value) => value?.toLowerCase().trim()
1177
- },
1178
- password: {
1179
- type: 'string',
1180
- required: true,
1181
- min: 8,
1182
- setter: async (value) => {
1183
- // Simulate password hashing
1184
- await new Promise(resolve => setTimeout(resolve, 10));
1185
- return `hashed:${value}`;
1186
- },
1187
- getter: () => '[PROTECTED]' // Never expose hashed passwords
1188
- }
1189
- },
1190
- tableName: 'users'
1191
- });
1192
-
1193
- const user = await api.resources.users.post({
1194
- email: ' USER@EXAMPLE.COM ',
1195
- password: 'mySecretPassword123'
1196
- });
1197
-
1198
- console.log('Created user:', user);
1199
- // {
1200
- // id: '1',
1201
- // email: 'user@example.com',
1202
- // password: '[PROTECTED]'
1203
- // }
1204
- ```
1205
-
1206
- ## Setter Dependencies
1207
-
1208
- When setters depend on other fields, use `runSetterAfter`:
1209
-
1210
- ```javascript
1211
- await api.addResource('products', {
1212
- schema: {
1213
- id: { type: 'id' },
1214
- name: { type: 'string', required: true },
1215
- base_price: {
1216
- type: 'number',
1217
- setter: (value) => Math.round(value * 100) // Convert to cents
1218
- },
1219
- tax_rate: {
1220
- type: 'number',
1221
- setter: (value) => value || 0
1222
- },
1223
- total_price: {
1224
- type: 'number',
1225
- setter: (value, { attributes }) => {
1226
- // Calculate from base_price (already in cents) and tax_rate
1227
- const total = attributes.base_price * (1 + attributes.tax_rate);
1228
- return Math.round(total);
1229
- },
1230
- runSetterAfter: ['base_price', 'tax_rate']
1231
- }
1232
- },
1233
- tableName: 'products'
1234
- });
1235
- ```
1236
-
1237
- ## Summary
1238
-
1239
- Getters and setters provide automatic data transformation:
1240
-
1241
- - **Getters** transform data when reading (including in relationships and queries)
1242
- - **Setters** transform data before storing
1243
- - Both support async operations and dependencies
1244
- - They work with actual database columns
1245
-
1246
- Common uses:
1247
- - Email normalization
1248
- - String trimming
1249
- - Title case formatting
1250
- - Password hashing
1251
- - Price calculations
1252
- - Data consistency across related records