json-rest-api 1.0.7 → 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.
- package/.claude/agents/library-guide-expert.md +29 -0
- package/.claude/agents/rest-api-internals-expert.md +53 -0
- package/CLAUDE.md +139 -1
- package/README.md +2 -5
- package/docTest.js +238 -0
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +955 -0
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +54 -0
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +750 -0
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +494 -0
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +398 -0
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +882 -0
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +709 -0
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +1233 -0
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +796 -0
- package/{README_X_Relationship_Plugin.md → docs/GUIDE/GUIDE_2_9_Relationships_Urls.md} +1 -4
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +66 -0
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +1246 -0
- package/docs/GUIDE/GUIDE_5_Non-Db_Stores.md +0 -0
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1490 -0
- package/{README_X_SocketIO.md → docs/GUIDE/GUIDE_X_SocketIO.md} +17 -17
- package/docs/GUIDE/index.md +58 -0
- package/docs/GUIDE.md +61 -2238
- package/docs/ONBOARDING/Computed_Properties_And_Sparse_Fields.md +398 -0
- package/docs/README.md +57 -2239
- package/{README_2_Computed_and_hidden_fields.md → docs/README_VARIOUS/GUIDE_2_Computed_and_hidden_fields.md} +22 -17
- package/docs/README_VARIOUS/GUIDE_IN_DOCS.md +2277 -0
- package/{README.ORIG.md → docs/README_VARIOUS/README.ORIG.md} +8 -8
- package/{README_8_Polymorphic_Relationships.md → docs/README_VARIOUS/README_8_Polymorphic_Relationships.md} +1 -1
- package/docs/README_VARIOUS/README_IN_DOCS.ms +2277 -0
- package/{RELATIONSHIPS_ADVANCED.md → docs/README_VARIOUS/RELATIONSHIPS_ADVANCED.md} +3 -3
- package/{correct-schema-format.md → docs/README_VARIOUS/correct-schema-format.md} +13 -13
- package/{pagination.md → docs/README_VARIOUS/pagination.md} +11 -8
- package/docs/assets/css/style.scss +78 -5
- package/docs/index.md +2 -12
- package/helper-reorganization-plan.md +85 -0
- package/helper-usage-analysis.md +94 -0
- package/package.json +5 -4
- package/plugins/core/bulk-operations-plugin.js +2 -2
- package/plugins/core/connectors/express-plugin.js +90 -32
- package/plugins/core/lib/querying/knex-cross-table-search.js +609 -0
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +491 -0
- package/plugins/core/lib/querying/knex-pagination-helpers.js +544 -0
- package/plugins/core/lib/{knex-process-includes.js → querying/knex-process-includes.js} +65 -50
- package/plugins/core/lib/querying/knex-query-helpers-base.js +74 -0
- package/plugins/core/lib/{knex-query-helpers.js → querying/knex-query-helpers.js} +416 -180
- package/plugins/core/lib/{knex-relationship-includes.js → querying/knex-relationship-includes.js} +430 -114
- package/plugins/core/lib/querying/knex-window-queries.js +328 -0
- package/plugins/core/lib/querying-writing/compile-schemas.js +328 -0
- package/plugins/core/{utils → lib/querying-writing}/connectors-query-parser.js +62 -57
- package/plugins/core/lib/{database-capabilities.js → querying-writing/database-capabilities.js} +113 -7
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +369 -0
- package/plugins/core/lib/querying-writing/default-data-helpers.js +72 -0
- package/plugins/core/{utils → lib/querying-writing}/field-utils.js +41 -31
- package/plugins/core/{utils → lib/querying-writing}/knex-constants.js +1 -0
- package/plugins/core/lib/{knex-field-helpers.js → querying-writing/knex-field-helpers.js} +190 -57
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +162 -0
- package/plugins/core/lib/{payload-validators.js → querying-writing/payload-validators.js} +297 -217
- package/plugins/core/lib/querying-writing/schema-helpers.js +497 -0
- package/plugins/core/lib/querying-writing/scope-validations.js +431 -0
- package/plugins/core/lib/{simplified-helpers.js → querying-writing/simplified-helpers.js} +216 -176
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +110 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +300 -0
- package/plugins/core/lib/writing/relationship-processor.js +252 -0
- package/plugins/core/rest-api-knex-plugin.js +183 -62
- package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +7 -0
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +72 -0
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +102 -0
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +50 -0
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +39 -0
- package/plugins/core/rest-api-plugin-methods/add-route.js +23 -0
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +18 -0
- package/plugins/core/rest-api-plugin-methods/common.js +605 -0
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +115 -0
- package/plugins/core/rest-api-plugin-methods/delete.js +109 -0
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +186 -0
- package/plugins/core/rest-api-plugin-methods/get-related.js +295 -0
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +62 -0
- package/plugins/core/rest-api-plugin-methods/get.js +187 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +56 -0
- package/plugins/core/rest-api-plugin-methods/patch.js +193 -0
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +104 -0
- package/plugins/core/rest-api-plugin-methods/post.js +157 -0
- package/plugins/core/rest-api-plugin-methods/put.js +257 -0
- package/plugins/core/rest-api-plugin-methods/query.js +208 -0
- package/plugins/core/rest-api-plugin.js +108 -2498
- package/plugins/core/socketio-plugin.js +28 -5
- package/plugins/storage/local-storage.js +2 -2
- package/tests/computed-fields.test.js +483 -0
- package/tests/cors.test.js +4 -4
- package/tests/curl.test.js +5 -4
- package/tests/custom-idproperty-relationships.test.js +891 -0
- package/tests/field-getters.test.js +353 -0
- package/tests/field-setters.test.js +310 -0
- package/tests/fixtures/api-configs.js +800 -28
- package/tests/jwt-auth.test.js +0 -2
- package/tests/nested-includes.test.js +1 -1
- package/tests/pagination-cursor-multifield.test.js +514 -0
- package/tests/pagination-enhanced.test.js +12 -9
- package/tests/polymorphic-relationships.test.js +10 -8
- package/tests/polymorphic-simple.test.js +6 -4
- package/tests/positioning-advanced.test.js +12 -12
- package/tests/positioning-extended.test.js +25 -25
- package/tests/positioning-multifilter.test.js +24 -24
- package/tests/positioning.test.js +3 -3
- package/tests/query-limits.test.js +24 -8
- package/tests/relationship-endpoints.test.js +4 -5
- package/tests/relationships.test.js +0 -1
- package/tests/socketio.test.js +12 -1
- package/tests/virtual-fields.test.js +13 -13
- package/PLAN_FOR_WEBSOCKETS.md +0 -398
- package/README_1_Initial_Setup.md +0 -393
- package/README_X_Hooks.md +0 -647
- package/TODO_rest-api-plugin-extras.todo +0 -23
- package/connectors_ideas/express-plugin.js +0 -120
- package/connectors_ideas/http-plugin.js +0 -138
- package/docs/examples/express-plugin.js +0 -366
- package/example.js +0 -301
- package/plugins/core/HELPER_MODULES.md +0 -285
- package/plugins/core/lib/compile-schemas.js +0 -245
- package/plugins/core/lib/database-value-normalizers.js +0 -192
- package/plugins/core/lib/default-data-helpers.js +0 -100
- package/plugins/core/lib/knex-cross-table-search.js +0 -442
- package/plugins/core/lib/knex-json-api-helpers.js +0 -84
- package/plugins/core/lib/knex-json-api-transformers.js +0 -449
- package/plugins/core/lib/knex-pagination-helpers.js +0 -193
- package/plugins/core/lib/knex-window-queries.js +0 -142
- package/plugins/core/lib/many-to-many-manipulations.js +0 -343
- package/plugins/core/lib/relationship-processor.js +0 -278
- package/plugins/core/lib/schema-helpers.js +0 -343
- package/plugins/core/lib/scope-validations.js +0 -252
- package/plugins/core/rest-api-relationships-plugin.js +0 -700
- package/plugins/core/utils/knex-query-helpers-base.js +0 -56
- /package/{README_X_Permissions.md → docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md} +0 -0
- /package/{README_X_Positioning.md → docs/GUIDE/GUIDE_6_Positioning.md} +0 -0
- /package/{README_X_Bulk_Operations.md → docs/GUIDE/GUIDE_X_Bulk_Operations.md} +0 -0
- /package/{README_X_Cors.md → docs/GUIDE/GUIDE_X_Cors.md} +0 -0
- /package/{README_X_Multihome.md → docs/GUIDE/GUIDE_X_Multihome.md} +0 -0
- /package/{README_X_Appendices.md → docs/GUIDE/GUIDE_Y_Appendices.md} +0 -0
- /package/{QUERY_DEPTH_PROTECTION.md → docs/README_VARIOUS/QUERY_DEPTH_PROTECTION.md} +0 -0
- /package/{RELATIONSHIPS.md → docs/README_VARIOUS/RELATIONSHIPS.md} +0 -0
- /package/{WEBSOCKET_IMPLEMENTATION_STATUS.md → docs/README_VARIOUS/WEBSOCKET_IMPLEMENTATION_STATUS.md} +0 -0
- /package/{helper-functions-chart.md → docs/README_VARIOUS/helper-functions-chart.md} +0 -0
- /package/docs/{implementation-hidden-computed-fields.md → README_VARIOUS/implementation-hidden-computed-fields.md} +0 -0
- /package/docs/{include-limits-migration-guide.md → README_VARIOUS/include-limits-migration-guide.md} +0 -0
- /package/{readmeSnippets → docs/README_VARIOUS}/test_put_behavior.js +0 -0
- /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
|