json-rest-api 1.0.6 → 1.0.8

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 (146) hide show
  1. package/.claude/agents/library-guide-expert.md +29 -0
  2. package/.claude/agents/rest-api-internals-expert.md +53 -0
  3. package/CLAUDE.md +139 -1
  4. package/README.md +2 -5
  5. package/docTest.js +238 -0
  6. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +955 -0
  7. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +54 -0
  8. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +750 -0
  9. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +494 -0
  10. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +398 -0
  11. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +882 -0
  12. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +709 -0
  13. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +1233 -0
  14. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +796 -0
  15. package/{README_X_Relationship_Plugin.md → docs/GUIDE/GUIDE_2_9_Relationships_Urls.md} +1 -4
  16. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +66 -0
  17. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +1246 -0
  18. package/docs/GUIDE/GUIDE_5_Non-Db_Stores.md +0 -0
  19. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1490 -0
  20. package/{README_X_SocketIO.md → docs/GUIDE/GUIDE_X_SocketIO.md} +17 -17
  21. package/docs/GUIDE/index.md +58 -0
  22. package/docs/GUIDE.md +61 -2238
  23. package/docs/ONBOARDING/Computed_Properties_And_Sparse_Fields.md +398 -0
  24. package/docs/README.md +57 -2239
  25. package/{README_2_Computed_and_hidden_fields.md → docs/README_VARIOUS/GUIDE_2_Computed_and_hidden_fields.md} +22 -17
  26. package/docs/README_VARIOUS/GUIDE_IN_DOCS.md +2277 -0
  27. package/{README.ORIG.md → docs/README_VARIOUS/README.ORIG.md} +8 -8
  28. package/{README_8_Polymorphic_Relationships.md → docs/README_VARIOUS/README_8_Polymorphic_Relationships.md} +1 -1
  29. package/docs/README_VARIOUS/README_IN_DOCS.ms +2277 -0
  30. package/{RELATIONSHIPS_ADVANCED.md → docs/README_VARIOUS/RELATIONSHIPS_ADVANCED.md} +3 -3
  31. package/{correct-schema-format.md → docs/README_VARIOUS/correct-schema-format.md} +13 -13
  32. package/{pagination.md → docs/README_VARIOUS/pagination.md} +11 -8
  33. package/docs/assets/css/style.scss +78 -5
  34. package/docs/index.md +2 -12
  35. package/helper-reorganization-plan.md +85 -0
  36. package/helper-usage-analysis.md +94 -0
  37. package/package.json +5 -4
  38. package/plugins/core/bulk-operations-plugin.js +2 -2
  39. package/plugins/core/connectors/express-plugin.js +99 -33
  40. package/plugins/core/lib/querying/knex-cross-table-search.js +609 -0
  41. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +491 -0
  42. package/plugins/core/lib/querying/knex-pagination-helpers.js +544 -0
  43. package/plugins/core/lib/{knex-process-includes.js → querying/knex-process-includes.js} +65 -50
  44. package/plugins/core/lib/querying/knex-query-helpers-base.js +74 -0
  45. package/plugins/core/lib/{knex-query-helpers.js → querying/knex-query-helpers.js} +416 -180
  46. package/plugins/core/lib/{knex-relationship-includes.js → querying/knex-relationship-includes.js} +430 -114
  47. package/plugins/core/lib/querying/knex-window-queries.js +328 -0
  48. package/plugins/core/lib/querying-writing/compile-schemas.js +328 -0
  49. package/plugins/core/{utils → lib/querying-writing}/connectors-query-parser.js +62 -57
  50. package/plugins/core/lib/{database-capabilities.js → querying-writing/database-capabilities.js} +113 -7
  51. package/plugins/core/lib/querying-writing/database-value-normalizers.js +369 -0
  52. package/plugins/core/lib/querying-writing/default-data-helpers.js +72 -0
  53. package/plugins/core/{utils → lib/querying-writing}/field-utils.js +41 -31
  54. package/plugins/core/{utils → lib/querying-writing}/knex-constants.js +1 -0
  55. package/plugins/core/lib/{knex-field-helpers.js → querying-writing/knex-field-helpers.js} +190 -57
  56. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +162 -0
  57. package/plugins/core/lib/{payload-validators.js → querying-writing/payload-validators.js} +297 -217
  58. package/plugins/core/lib/querying-writing/schema-helpers.js +497 -0
  59. package/plugins/core/lib/querying-writing/scope-validations.js +431 -0
  60. package/plugins/core/lib/{simplified-helpers.js → querying-writing/simplified-helpers.js} +216 -176
  61. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +110 -0
  62. package/plugins/core/lib/writing/many-to-many-manipulations.js +300 -0
  63. package/plugins/core/lib/writing/relationship-processor.js +252 -0
  64. package/plugins/core/rest-api-knex-plugin.js +183 -62
  65. package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +7 -0
  66. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +72 -0
  67. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +102 -0
  68. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +50 -0
  69. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +39 -0
  70. package/plugins/core/rest-api-plugin-methods/add-route.js +23 -0
  71. package/plugins/core/rest-api-plugin-methods/check-permissions.js +18 -0
  72. package/plugins/core/rest-api-plugin-methods/common.js +605 -0
  73. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +115 -0
  74. package/plugins/core/rest-api-plugin-methods/delete.js +109 -0
  75. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +186 -0
  76. package/plugins/core/rest-api-plugin-methods/get-related.js +295 -0
  77. package/plugins/core/rest-api-plugin-methods/get-relationship.js +62 -0
  78. package/plugins/core/rest-api-plugin-methods/get.js +187 -0
  79. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +56 -0
  80. package/plugins/core/rest-api-plugin-methods/patch.js +193 -0
  81. package/plugins/core/rest-api-plugin-methods/post-relationship.js +104 -0
  82. package/plugins/core/rest-api-plugin-methods/post.js +157 -0
  83. package/plugins/core/rest-api-plugin-methods/put.js +257 -0
  84. package/plugins/core/rest-api-plugin-methods/query.js +208 -0
  85. package/plugins/core/rest-api-plugin.js +108 -2498
  86. package/plugins/core/socketio-plugin.js +28 -5
  87. package/plugins/storage/local-storage.js +2 -2
  88. package/tests/computed-fields.test.js +483 -0
  89. package/tests/cors.test.js +4 -4
  90. package/tests/curl.test.js +5 -4
  91. package/tests/custom-idproperty-relationships.test.js +891 -0
  92. package/tests/field-getters.test.js +353 -0
  93. package/tests/field-setters.test.js +310 -0
  94. package/tests/fixtures/api-configs.js +800 -28
  95. package/tests/jwt-auth.test.js +0 -2
  96. package/tests/nested-includes.test.js +1 -1
  97. package/tests/pagination-cursor-multifield.test.js +514 -0
  98. package/tests/pagination-enhanced.test.js +12 -9
  99. package/tests/polymorphic-relationships.test.js +10 -8
  100. package/tests/polymorphic-simple.test.js +6 -4
  101. package/tests/positioning-advanced.test.js +12 -12
  102. package/tests/positioning-extended.test.js +25 -25
  103. package/tests/positioning-multifilter.test.js +24 -24
  104. package/tests/positioning.test.js +3 -3
  105. package/tests/query-limits.test.js +24 -8
  106. package/tests/relationship-endpoints.test.js +4 -5
  107. package/tests/relationships.test.js +0 -1
  108. package/tests/socketio.test.js +12 -1
  109. package/tests/virtual-fields.test.js +13 -13
  110. package/PLAN_FOR_WEBSOCKETS.md +0 -398
  111. package/README_1_Initial_Setup.md +0 -393
  112. package/README_X_Hooks.md +0 -647
  113. package/TODO_rest-api-plugin-extras.todo +0 -23
  114. package/connectors_ideas/express-plugin.js +0 -120
  115. package/connectors_ideas/http-plugin.js +0 -138
  116. package/docs/examples/express-plugin.js +0 -366
  117. package/example.js +0 -301
  118. package/plugins/core/HELPER_MODULES.md +0 -285
  119. package/plugins/core/lib/compile-schemas.js +0 -245
  120. package/plugins/core/lib/database-value-normalizers.js +0 -192
  121. package/plugins/core/lib/default-data-helpers.js +0 -100
  122. package/plugins/core/lib/knex-cross-table-search.js +0 -442
  123. package/plugins/core/lib/knex-json-api-helpers.js +0 -84
  124. package/plugins/core/lib/knex-json-api-transformers.js +0 -449
  125. package/plugins/core/lib/knex-pagination-helpers.js +0 -193
  126. package/plugins/core/lib/knex-window-queries.js +0 -142
  127. package/plugins/core/lib/many-to-many-manipulations.js +0 -343
  128. package/plugins/core/lib/relationship-processor.js +0 -278
  129. package/plugins/core/lib/schema-helpers.js +0 -343
  130. package/plugins/core/lib/scope-validations.js +0 -252
  131. package/plugins/core/rest-api-relationships-plugin.js +0 -700
  132. package/plugins/core/utils/knex-query-helpers-base.js +0 -56
  133. /package/{README_X_Permissions.md → docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md} +0 -0
  134. /package/{README_X_Positioning.md → docs/GUIDE/GUIDE_6_Positioning.md} +0 -0
  135. /package/{README_X_Bulk_Operations.md → docs/GUIDE/GUIDE_X_Bulk_Operations.md} +0 -0
  136. /package/{README_X_Cors.md → docs/GUIDE/GUIDE_X_Cors.md} +0 -0
  137. /package/{README_X_Multihome.md → docs/GUIDE/GUIDE_X_Multihome.md} +0 -0
  138. /package/{README_X_Appendices.md → docs/GUIDE/GUIDE_Y_Appendices.md} +0 -0
  139. /package/{QUERY_DEPTH_PROTECTION.md → docs/README_VARIOUS/QUERY_DEPTH_PROTECTION.md} +0 -0
  140. /package/{RELATIONSHIPS.md → docs/README_VARIOUS/RELATIONSHIPS.md} +0 -0
  141. /package/{WEBSOCKET_IMPLEMENTATION_STATUS.md → docs/README_VARIOUS/WEBSOCKET_IMPLEMENTATION_STATUS.md} +0 -0
  142. /package/{helper-functions-chart.md → docs/README_VARIOUS/helper-functions-chart.md} +0 -0
  143. /package/docs/{implementation-hidden-computed-fields.md → README_VARIOUS/implementation-hidden-computed-fields.md} +0 -0
  144. /package/docs/{include-limits-migration-guide.md → README_VARIOUS/include-limits-migration-guide.md} +0 -0
  145. /package/{readmeSnippets → docs/README_VARIOUS}/test_put_behavior.js +0 -0
  146. /package/{readmeSnippets → docs/README_VARIOUS}/test_put_fields_without_defaults.js +0 -0
@@ -0,0 +1,398 @@
1
+ # Computed Properties and Sparse Fields: Deep Dive
2
+
3
+ This guide explains how computed fields, sparse fieldsets, and the `enrichAttributes` method work together in the json-rest-api library.
4
+
5
+ ## Table of Contents
6
+ 1. [Overview](#overview)
7
+ 2. [Key Concepts](#key-concepts)
8
+ 3. [The Data Flow](#the-data-flow)
9
+ 4. [enrichAttributes Method](#enrichattributes-method)
10
+ 5. [Computed Dependencies](#computed-dependencies)
11
+ 6. [Sparse Fieldsets](#sparse-fieldsets)
12
+ 7. [Complete Example Walkthrough](#complete-example-walkthrough)
13
+
14
+ ## Overview
15
+
16
+ The json-rest-api library supports computed fields - virtual fields that are calculated on-the-fly based on other fields. When combined with sparse fieldsets, the system intelligently fetches only the necessary dependencies from the database, computes the requested fields, and then removes any dependencies that weren't explicitly requested.
17
+
18
+ ## Key Concepts
19
+
20
+ ### Computed Fields
21
+ Fields that don't exist in the database but are calculated from other fields:
22
+
23
+ ```javascript
24
+ await api.addResource('products', {
25
+ schema: {
26
+ name: { type: 'string', required: true },
27
+ price: { type: 'number' },
28
+ cost: { type: 'number', normallyHidden: true },
29
+ profit_margin: {
30
+ type: 'number',
31
+ computed: true, // Mark as computed field
32
+ dependencies: ['price', 'cost'],
33
+ compute: ({ attributes }) => {
34
+ if (!attributes.price || !attributes.cost) return null;
35
+ return ((attributes.price - attributes.cost) / attributes.price * 100).toFixed(2);
36
+ }
37
+ }
38
+ }
39
+ });
40
+ ```
41
+
42
+ ### Sparse Fieldsets
43
+ JSON:API feature allowing clients to request only specific fields:
44
+ ```
45
+ GET /products/1?fields[products]=name,profit_margin
46
+ ```
47
+
48
+ ### Computed Dependencies
49
+ Fields that are needed to compute other fields but weren't explicitly requested. In the above example, if only `profit_margin` is requested, both `price` and `cost` are computed dependencies.
50
+
51
+ ## The Data Flow
52
+
53
+ Here's the complete flow when a request with sparse fieldsets and computed fields is made:
54
+
55
+ ```
56
+ 1. Request arrives: GET /products/1?fields[products]=name,profit_margin
57
+ ↓
58
+ 2. buildFieldSelection() determines what to SELECT from database
59
+ ↓
60
+ 3. Database query: SELECT id, name, price, cost FROM products WHERE id = 1
61
+ ↓
62
+ 4. Raw data: { id: 1, name: 'Widget', price: 100, cost: 60 }
63
+ ↓
64
+ 5. enrichAttributes() is called to compute fields and filter data
65
+ ↓
66
+ 6. Final response: { id: '1', name: 'Widget', profit_margin: '40.00' }
67
+ ```
68
+
69
+ ## enrichAttributes Method
70
+
71
+ ### When It's Called
72
+
73
+ `enrichAttributes` is called in two main scenarios:
74
+
75
+ 1. **For main resources** - in `dataGetQuery` and `dataGet` methods:
76
+ ```javascript
77
+ // rest-api-plugin.js, line 497
78
+ entry.attributes = await scope.enrichAttributes({
79
+ attributes: entry.attributes,
80
+ parentContext: context,
81
+ requestedComputedFields: requestedComputedFields,
82
+ isMainResource: true,
83
+ computedDependencies: context.computedDependencies
84
+ })
85
+ ```
86
+
87
+ 2. **For included resources** - when processing relationships:
88
+ ```javascript
89
+ // rest-api-plugin.js, line 515
90
+ entry.attributes = await entryScope.enrichAttributes({
91
+ attributes: entry.attributes,
92
+ parentContext: context,
93
+ requestedComputedFields: entryRequestedComputed,
94
+ isMainResource: false,
95
+ computedDependencies: entry.__$jsonrestapi_computed_deps$__
96
+ })
97
+ ```
98
+
99
+ ### Parameters
100
+
101
+ ```javascript
102
+ {
103
+ attributes, // Raw attributes from database
104
+ parentContext, // Original request context (has queryParams, transaction, etc.)
105
+ requestedComputedFields, // Array of computed fields to calculate
106
+ isMainResource, // Boolean - true for main, false for included
107
+ computedDependencies // Array of fields to remove if not explicitly requested
108
+ }
109
+ ```
110
+
111
+ ### What It Does
112
+
113
+ Here's the implementation with annotations:
114
+
115
+ ```javascript
116
+ // rest-api-plugin.js, line 2368
117
+ addScopeMethod('enrichAttributes', async ({ context, params, runHooks, scopeName, scopes, api, helpers }) => {
118
+ const { attributes, parentContext, requestedComputedFields, isMainResource, computedDependencies } = params || {};
119
+
120
+ // Step 1: Get schema information
121
+ const schemaStructure = scopes[scopeName]?.vars?.schemaInfo?.schemaStructure || {};
122
+ const computedFields = scopes[scopeName]?.vars?.schemaInfo?.computed || {};
123
+
124
+ // Step 2: Filter hidden fields (removes hidden:true and normallyHidden:true unless requested)
125
+ const requestedFields = parentContext?.queryParams?.fields?.[scopeName];
126
+ const filteredAttributes = filterHiddenFields(attributes, { structure: schemaStructure }, requestedFields);
127
+
128
+ // Step 3: Handle virtual fields based on sparse fieldsets
129
+ // Virtual fields (marked with virtual: true in schema) are already in filteredAttributes
130
+ // We need to remove them if they weren't requested in sparse fieldsets
131
+ if (requestedFields && requestedFields.length > 0) {
132
+ Object.entries(schemaStructure).forEach(([fieldName, fieldDef]) => {
133
+ if (fieldDef.virtual === true && fieldName in filteredAttributes) {
134
+ if (!requestedFields.includes(fieldName)) {
135
+ delete filteredAttributes[fieldName];
136
+ }
137
+ }
138
+ });
139
+ }
140
+
141
+ // Step 4: Determine which computed fields to calculate
142
+ let fieldsToCompute = [];
143
+ if (requestedComputedFields) {
144
+ fieldsToCompute = requestedComputedFields;
145
+ } else if (isMainResource || !parentContext?.queryParams?.fields) {
146
+ fieldsToCompute = Object.keys(computedFields);
147
+ }
148
+
149
+ // Step 5: Calculate computed fields
150
+ const computeContext = {
151
+ attributes: attributes, // ALL attributes including dependencies
152
+ record: { ...attributes },
153
+ context: parentContext,
154
+ helpers,
155
+ api,
156
+ };
157
+
158
+ for (const fieldName of fieldsToCompute) {
159
+ const fieldDef = computedFields[fieldName];
160
+ if (fieldDef && fieldDef.compute) {
161
+ try {
162
+ filteredAttributes[fieldName] = await fieldDef.compute(computeContext);
163
+ } catch (error) {
164
+ console.error(`Error computing field '${fieldName}' for ${scopeName}:`, error);
165
+ filteredAttributes[fieldName] = null;
166
+ }
167
+ }
168
+ }
169
+
170
+ // Step 6: Remove computed dependencies that weren't explicitly requested
171
+ const finalAttributes = { ...filteredAttributes };
172
+ if (requestedFields && computedDependencies && computedDependencies.length > 0) {
173
+ const requested = typeof requestedFields === 'string'
174
+ ? requestedFields.split(',').map(f => f.trim()).filter(f => f)
175
+ : requestedFields;
176
+
177
+ for (const dep of computedDependencies) {
178
+ if (!requested.includes(dep)) {
179
+ delete finalAttributes[dep];
180
+ }
181
+ }
182
+ }
183
+
184
+ // Step 7: Run enrichAttributes hooks
185
+ Object.assign(context, {
186
+ parentContext,
187
+ attributes: finalAttributes,
188
+ computedFields,
189
+ requestedComputedFields: fieldsToCompute,
190
+ scopeName,
191
+ helpers,
192
+ api
193
+ });
194
+
195
+ await runHooks('enrichAttributes');
196
+
197
+ return context.attributes;
198
+ });
199
+ ```
200
+
201
+ ## Computed Dependencies
202
+
203
+ ### How They're Determined
204
+
205
+ In `buildFieldSelection` (knex-field-helpers.js):
206
+
207
+ ```javascript
208
+ // When a computed field is requested, add its dependencies
209
+ if (computedDef && computedDef.dependencies) {
210
+ for (const dep of computedDef.dependencies) {
211
+ if (!fieldsToSelect.has(dep)) {
212
+ fieldsToSelect.add(dep);
213
+ // Track that this is a dependency (not explicitly requested)
214
+ if (!requested.includes(dep)) {
215
+ computedDependencies.add(dep);
216
+ }
217
+ }
218
+ }
219
+ }
220
+ ```
221
+
222
+ ### Flow Through the System
223
+
224
+ 1. **Database Query Level**: Dependencies are added to SELECT clause
225
+ 2. **Knex Plugin**: Passes `computedDependencies` in context
226
+ 3. **REST API Plugin**: Receives them in `context.computedDependencies`
227
+ 4. **enrichAttributes**: Removes them if not explicitly requested
228
+
229
+ ### For Included Resources
230
+
231
+ The fix we implemented ensures computed dependencies flow through for included resources:
232
+
233
+ ```javascript
234
+ // In knex-relationship-includes.js, when creating JSON:API records:
235
+ if (fieldSelectionInfo?.computedDependencies) {
236
+ jsonApiRecord[COMPUTED_DEPENDENCIES_KEY] = fieldSelectionInfo.computedDependencies;
237
+ }
238
+
239
+ // Then in rest-api-plugin.js when processing includes:
240
+ entry.attributes = await entryScope.enrichAttributes({
241
+ // ...
242
+ computedDependencies: entry.__$jsonrestapi_computed_deps$__
243
+ })
244
+ ```
245
+
246
+ ## Sparse Fieldsets
247
+
248
+ ### Implementation Details
249
+
250
+ 1. **Field Validation**: The system validates requested fields exist:
251
+ ```javascript
252
+ // knex-field-helpers.js
253
+ if (!fieldDef) throw new Error(`Unknown sparse field '${field}' requested for '${scopeName}'`);
254
+ ```
255
+
256
+ 2. **Hidden Field Handling**:
257
+ - `hidden: true` fields are NEVER returned
258
+ - `normallyHidden: true` fields are returned ONLY if explicitly requested
259
+
260
+ 3. **Computed Field Handling**: Computed fields can be requested like regular fields
261
+
262
+ ## Complete Example Walkthrough
263
+
264
+ Let's trace a request through the system:
265
+
266
+ ### Request
267
+ ```
268
+ GET /users/1?include=posts&fields[users]=name,profit_margin&fields[posts]=title,popularity
269
+ ```
270
+
271
+ ### Step 1: Build Field Selection for Main Resource
272
+
273
+ ```javascript
274
+ // For users (main resource)
275
+ buildFieldSelection(usersScope, {
276
+ context: {
277
+ queryParams: { fields: { users: 'name,profit_margin' } }
278
+ }
279
+ })
280
+ // Returns:
281
+ {
282
+ fieldsToSelect: ['id', 'name', 'revenue', 'cost'], // Added dependencies
283
+ requestedFields: ['name', 'profit_margin'],
284
+ computedDependencies: ['revenue', 'cost'], // To be removed later
285
+ idProperty: 'id'
286
+ }
287
+ ```
288
+
289
+ ### Step 2: Database Queries
290
+
291
+ ```sql
292
+ -- Main resource
293
+ SELECT id, name, revenue, cost FROM users WHERE id = 1
294
+
295
+ -- Included posts (hasMany)
296
+ SELECT id, title, views, internal_score, author_id
297
+ FROM posts
298
+ WHERE author_id IN (1)
299
+ ```
300
+
301
+ ### Step 3: enrichAttributes for Main Resource
302
+
303
+ ```javascript
304
+ // Input attributes from DB
305
+ { id: 1, name: 'John Doe', revenue: 100000, cost: 60000 }
306
+
307
+ // After enrichAttributes
308
+ { id: '1', name: 'John Doe', profit_margin: '40.00' }
309
+ // Note: revenue and cost are removed as computed dependencies
310
+ ```
311
+
312
+ ### Step 4: enrichAttributes for Included Resources
313
+
314
+ ```javascript
315
+ // Each post goes through enrichAttributes
316
+ // Input: { id: 1, title: 'First Post', views: 150, internal_score: 20, author_id: 1 }
317
+ // Output: { id: '1', title: 'First Post', popularity: 'popular' }
318
+ // Note: views and internal_score removed as dependencies
319
+ ```
320
+
321
+ ### Step 5: Final JSON:API Response
322
+
323
+ ```json
324
+ {
325
+ "data": {
326
+ "type": "users",
327
+ "id": "1",
328
+ "attributes": {
329
+ "name": "John Doe",
330
+ "profit_margin": "40.00"
331
+ },
332
+ "relationships": {
333
+ "posts": {
334
+ "data": [
335
+ { "type": "posts", "id": "1" },
336
+ { "type": "posts", "id": "2" }
337
+ ]
338
+ }
339
+ }
340
+ },
341
+ "included": [
342
+ {
343
+ "type": "posts",
344
+ "id": "1",
345
+ "attributes": {
346
+ "title": "First Post",
347
+ "popularity": "popular"
348
+ }
349
+ },
350
+ {
351
+ "type": "posts",
352
+ "id": "2",
353
+ "attributes": {
354
+ "title": "Second Post",
355
+ "popularity": "normal"
356
+ }
357
+ }
358
+ ]
359
+ }
360
+ ```
361
+
362
+ ### Step 6: Transformation to Simplified Format
363
+
364
+ ```javascript
365
+ {
366
+ "id": "1",
367
+ "name": "John Doe",
368
+ "profit_margin": "40.00",
369
+ "posts_ids": ["1", "2"], // Added from relationships
370
+ "posts": [
371
+ {
372
+ "id": "1",
373
+ "title": "First Post",
374
+ "popularity": "popular"
375
+ },
376
+ {
377
+ "id": "2",
378
+ "title": "Second Post",
379
+ "popularity": "normal"
380
+ }
381
+ ]
382
+ }
383
+ ```
384
+
385
+ ## Key Takeaways
386
+
387
+ 1. **enrichAttributes is the central processing point** for computed fields and field filtering
388
+ 2. **Computed dependencies are tracked throughout the pipeline** to ensure they're available for computation but removed from final output
389
+ 3. **The fix for included resources** ensures the same dependency removal logic applies to all resources
390
+ 4. **Sparse fieldsets control attributes**, not relationships (hence `_ids` fields always appear)
391
+ 5. **The system is efficient** - it only fetches and computes what's necessary
392
+
393
+ ## Debugging Tips
394
+
395
+ 1. Check `context.computedDependencies` to see what will be removed
396
+ 2. Use `console.log` in `enrichAttributes` to trace field transformations
397
+ 3. Verify `fieldSelectionInfo` contains expected dependencies
398
+ 4. Remember that `_ids` fields are relationships, not attributes