json-rest-api 1.0.14 → 1.0.16

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 (118) hide show
  1. package/connectors_ideas/fastify.js +66 -68
  2. package/connectors_ideas/koa-plugin.js +50 -51
  3. package/eslint.config.js +11 -2
  4. package/examples/url-override-example.js +63 -63
  5. package/index.js +13 -13
  6. package/lib/enhanced-logger.js +51 -51
  7. package/lib/error-formatter.js +61 -61
  8. package/lib/rest-api-errors.js +28 -28
  9. package/package.json +1 -1
  10. package/plugins/core/bulk-operations-plugin.js +142 -145
  11. package/plugins/core/connectors/express-plugin.js +210 -217
  12. package/plugins/core/connectors/lib/busboy-detector.js +84 -84
  13. package/plugins/core/connectors/lib/formidable-detector.js +50 -50
  14. package/plugins/core/connectors/lib/request-helpers.js +74 -74
  15. package/plugins/core/file-handling-plugin.js +125 -125
  16. package/plugins/core/lib/anyapi/anyapi-registry.js +254 -254
  17. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +287 -287
  18. package/plugins/core/lib/anyapi/schema-utils.js +101 -101
  19. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +41 -41
  20. package/plugins/core/lib/dbTablesOperations.js +134 -135
  21. package/plugins/core/lib/fractional-positioning.js +74 -76
  22. package/plugins/core/lib/querying/knex-cross-table-search.js +320 -320
  23. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +144 -144
  24. package/plugins/core/lib/querying/knex-pagination-helpers.js +164 -164
  25. package/plugins/core/lib/querying/knex-process-includes.js +50 -50
  26. package/plugins/core/lib/querying/knex-query-helpers-base.js +31 -31
  27. package/plugins/core/lib/querying/knex-query-helpers.js +451 -452
  28. package/plugins/core/lib/querying/knex-relationship-includes.js +1160 -1170
  29. package/plugins/core/lib/querying/knex-window-queries.js +89 -89
  30. package/plugins/core/lib/querying/url-helpers.js +35 -35
  31. package/plugins/core/lib/querying-writing/compile-schemas.js +99 -99
  32. package/plugins/core/lib/querying-writing/connectors-query-parser.js +31 -31
  33. package/plugins/core/lib/querying-writing/database-capabilities.js +94 -94
  34. package/plugins/core/lib/querying-writing/database-value-normalizers.js +76 -76
  35. package/plugins/core/lib/querying-writing/default-data-helpers.js +27 -27
  36. package/plugins/core/lib/querying-writing/field-utils.js +44 -42
  37. package/plugins/core/lib/querying-writing/knex-constants.js +15 -16
  38. package/plugins/core/lib/querying-writing/knex-field-helpers.js +117 -115
  39. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +41 -41
  40. package/plugins/core/lib/querying-writing/payload-validators.js +275 -275
  41. package/plugins/core/lib/querying-writing/schema-helpers.js +129 -129
  42. package/plugins/core/lib/querying-writing/scope-validations.js +143 -146
  43. package/plugins/core/lib/querying-writing/simplified-helpers.js +133 -129
  44. package/plugins/core/lib/storage/storage-adapter.js +68 -68
  45. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +31 -31
  46. package/plugins/core/lib/writing/many-to-many-manipulations.js +81 -81
  47. package/plugins/core/lib/writing/relationship-processor.js +58 -58
  48. package/plugins/core/rest-api-access.js +11 -15
  49. package/plugins/core/rest-api-anyapi-knex-plugin.js +920 -920
  50. package/plugins/core/rest-api-cors-plugin.js +170 -171
  51. package/plugins/core/rest-api-knex-plugin.js +482 -493
  52. package/plugins/core/rest-api-multihome-plugin.js +247 -251
  53. package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +5 -5
  54. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +20 -21
  55. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +44 -44
  56. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +23 -23
  57. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +15 -15
  58. package/plugins/core/rest-api-plugin-methods/add-route.js +9 -9
  59. package/plugins/core/rest-api-plugin-methods/check-permissions.js +5 -6
  60. package/plugins/core/rest-api-plugin-methods/common.js +295 -297
  61. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +35 -37
  62. package/plugins/core/rest-api-plugin-methods/delete.js +57 -59
  63. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +172 -171
  64. package/plugins/core/rest-api-plugin-methods/get-related.js +96 -96
  65. package/plugins/core/rest-api-plugin-methods/get-relationship.js +16 -18
  66. package/plugins/core/rest-api-plugin-methods/get.js +86 -86
  67. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +16 -17
  68. package/plugins/core/rest-api-plugin-methods/patch.js +63 -64
  69. package/plugins/core/rest-api-plugin-methods/post-relationship.js +54 -55
  70. package/plugins/core/rest-api-plugin-methods/post.js +61 -62
  71. package/plugins/core/rest-api-plugin-methods/put.js +86 -87
  72. package/plugins/core/rest-api-plugin-methods/query.js +88 -88
  73. package/plugins/core/rest-api-plugin-methods/release.js +5 -6
  74. package/plugins/core/rest-api-plugin.js +76 -78
  75. package/plugins/core/rest-api-positioning-plugin.js +251 -252
  76. package/plugins/core/socketio-plugin.js +240 -236
  77. package/plugins/storage/local-storage.js +103 -103
  78. package/plugins/storage/s3-storage.js +41 -42
  79. package/quickTest.js +37 -37
  80. package/scripts/debug-anyapi-review-filter.mjs +18 -18
  81. package/scripts/inspect-anyapi-descriptor-country.mjs +10 -10
  82. package/scripts/inspect-anyapi-descriptor.mjs +10 -10
  83. package/scripts/run-docs.js +112 -113
  84. package/test-debug.js +28 -28
  85. package/tests/TEST_TEMPLATE.test.js +51 -51
  86. package/tests/anyapi-basic.test.js +42 -42
  87. package/tests/anyapi-cursor-pagination.test.js +48 -48
  88. package/tests/bulk-operations.test.js +208 -208
  89. package/tests/computed-fields.test.js +204 -204
  90. package/tests/cors.test.js +412 -413
  91. package/tests/curl.test.js +275 -276
  92. package/tests/custom-idproperty-relationships.test.js +266 -266
  93. package/tests/field-getters.test.js +132 -132
  94. package/tests/field-setters.test.js +110 -110
  95. package/tests/fixtures/api-configs.js +699 -699
  96. package/tests/helpers/storage-mode.js +32 -32
  97. package/tests/helpers/test-utils.js +76 -76
  98. package/tests/include-depth-validation.test.js +76 -76
  99. package/tests/includes.test.js +145 -145
  100. package/tests/multihome.test.js +175 -175
  101. package/tests/nested-includes.test.js +255 -255
  102. package/tests/pagination-cursor-multifield.test.js +164 -164
  103. package/tests/pagination-enhanced.test.js +225 -225
  104. package/tests/polymorphic-relationships.test.js +96 -96
  105. package/tests/polymorphic-simple.test.js +44 -44
  106. package/tests/positioning-advanced.test.js +226 -226
  107. package/tests/positioning-extended.test.js +399 -399
  108. package/tests/positioning-multifilter.test.js +204 -204
  109. package/tests/positioning.test.js +266 -266
  110. package/tests/queries.test.js +184 -184
  111. package/tests/query-limits.test.js +154 -155
  112. package/tests/relationship-endpoints.test.js +127 -127
  113. package/tests/relationships.test.js +178 -179
  114. package/tests/rest-api-access.test.js +86 -86
  115. package/tests/rest-api.test.js +103 -103
  116. package/tests/searchschema-merge.test.js +93 -93
  117. package/tests/socketio.test.js +326 -326
  118. package/tests/virtual-fields.test.js +164 -164
@@ -1,198 +1,199 @@
1
- import { filterHiddenFields } from '../lib/querying-writing/field-utils.js';
1
+ import { filterHiddenFields } from '../lib/querying-writing/field-utils.js'
2
2
 
3
3
  /**
4
4
  * enrichAttributes
5
5
  * Runs the enrichAttributes hook for a specific scope to allow plugins to modify attributes
6
6
  * before they are returned to the client. This is a scope method so each resource type
7
7
  * can have its own attribute enrichment logic.
8
- *
9
- *
8
+ *
9
+ *
10
10
  */
11
- export default async function enrichAttributesMethod({ context, params, runHooks, scopeName, scopes, api, helpers }) {
11
+ export default async function enrichAttributesMethod ({ context, params, runHooks, scopeName, scopes, api, helpers }) {
12
12
  // Extract parameters passed to enrichAttributes
13
13
  // - attributes: The raw attributes from database
14
14
  // - parentContext: The context from the calling method (has queryParams, transaction, etc.)
15
15
  // - requestedComputedFields: Which computed fields to calculate (from sparse fieldsets)
16
16
  // - isMainResource: Whether this is the main resource or an included one
17
17
  // - computedDependencies: Fields fetched only for computation (to be removed)
18
- // Extract parameters passed to enrichAttributes
19
- // - attributes: The raw attributes from database
20
- // - parentContext: The context from the calling method (has queryParams, transaction, etc.)
21
- // - requestedComputedFields: Which computed fields to calculate (from sparse fieldsets)
22
- // - isMainResource: Whether this is the main resource or an included one
23
- // - computedDependencies: Fields fetched only for computation (to be removed)
24
- const { attributes, parentContext, requestedComputedFields, isMainResource, computedDependencies } = params || {};
25
-
26
- // Return empty object if no attributes provided
27
- if (!attributes) {
28
- return {};
29
- }
18
+ // Extract parameters passed to enrichAttributes
19
+ // - attributes: The raw attributes from database
20
+ // - parentContext: The context from the calling method (has queryParams, transaction, etc.)
21
+ // - requestedComputedFields: Which computed fields to calculate (from sparse fieldsets)
22
+ // - isMainResource: Whether this is the main resource or an included one
23
+ // - computedDependencies: Fields fetched only for computation (to be removed)
24
+ const { attributes, parentContext, requestedComputedFields, isMainResource, computedDependencies } = params || {}
25
+
26
+ // Return empty object if no attributes provided
27
+ if (!attributes) {
28
+ return {}
29
+ }
30
+
31
+ // Get schema, computed field definitions
32
+ const schemaStructure = scopes[scopeName]?.vars?.schemaInfo?.schemaStructure || {}
33
+ const computedFields = scopes[scopeName]?.vars?.schemaInfo?.computed || {}
34
+ const fieldGetters = scopes[scopeName]?.vars?.schemaInfo?.fieldGetters || {}
35
+ const sortedGetterFields = scopes[scopeName]?.vars?.schemaInfo?.sortedGetterFields || []
30
36
 
31
- // Get schema, computed field definitions
32
- const schemaStructure = scopes[scopeName]?.vars?.schemaInfo?.schemaStructure || {};
33
- const computedFields = scopes[scopeName]?.vars?.schemaInfo?.computed || {};
34
- const fieldGetters = scopes[scopeName]?.vars?.schemaInfo?.fieldGetters || {};
35
- const sortedGetterFields = scopes[scopeName]?.vars?.schemaInfo?.sortedGetterFields || [];
36
-
37
- // Make a copy of attributes for transformation
38
- let transformedAttributes = { ...attributes };
39
-
40
- // STEP 1: Apply field getters in dependency order (before filtering)
41
- // This ensures getters see all fields including hidden ones
42
- for (const fieldName of sortedGetterFields) {
43
- // Only process if field exists in attributes
44
- if (fieldName in transformedAttributes) {
45
- const getterInfo = fieldGetters[fieldName];
46
- try {
47
- const getterContext = {
48
- attributes: transformedAttributes, // Current state with previous getters applied
49
- fieldName,
50
- originalValue: attributes[fieldName], // Original untransformed value
51
- originalAttributes: attributes, // All original attributes
52
- record: transformedAttributes, // Alias for compatibility
53
- parentContext,
54
- scopeName,
55
- api,
56
- helpers,
57
- isMainResource: isMainResource !== false
58
- };
59
-
60
- // Apply getter (can be async)
61
- transformedAttributes[fieldName] = await getterInfo.getter(
62
- transformedAttributes[fieldName],
63
- getterContext
64
- );
65
- } catch (error) {
66
- console.error(`Error in getter for field '${fieldName}' in ${scopeName}:`, error);
67
- // Keep current value on error (don't break the whole request)
68
- }
37
+ // Make a copy of attributes for transformation
38
+ const transformedAttributes = { ...attributes }
39
+
40
+ // STEP 1: Apply field getters in dependency order (before filtering)
41
+ // This ensures getters see all fields including hidden ones
42
+ for (const fieldName of sortedGetterFields) {
43
+ // Only process if field exists in attributes
44
+ if (fieldName in transformedAttributes) {
45
+ const getterInfo = fieldGetters[fieldName]
46
+ try {
47
+ const getterContext = {
48
+ attributes: transformedAttributes, // Current state with previous getters applied
49
+ fieldName,
50
+ originalValue: attributes[fieldName], // Original untransformed value
51
+ originalAttributes: attributes, // All original attributes
52
+ record: transformedAttributes, // Alias for compatibility
53
+ parentContext,
54
+ scopeName,
55
+ api,
56
+ helpers,
57
+ isMainResource: isMainResource !== false
69
58
  }
70
- }
71
59
 
72
- // Filter hidden fields from attributes based on visibility rules
73
- // This removes hidden:true fields and normallyHidden:true fields (unless requested)
74
- const requestedFields = parentContext?.queryParams?.fields?.[scopeName];
75
- const filteredAttributes = filterHiddenFields(transformedAttributes, { structure: schemaStructure }, requestedFields);
76
-
77
- // Handle virtual fields - they need to be added from parent context if available
78
- // Virtual fields come from input and need to be preserved in responses
79
- if (parentContext && parentContext.inputRecord?.data?.attributes) {
80
- const inputAttrs = parentContext.inputRecord.data.attributes;
81
- Object.entries(schemaStructure).forEach(([fieldName, fieldDef]) => {
82
- if (fieldDef.virtual === true && fieldName in inputAttrs && inputAttrs[fieldName] !== undefined && inputAttrs[fieldName] !== null) {
83
- // Add virtual field to filtered attributes if not already there
84
- if (!(fieldName in filteredAttributes)) {
85
- filteredAttributes[fieldName] = inputAttrs[fieldName];
86
- }
87
- }
88
- });
89
- }
90
-
91
- // Now handle sparse fieldsets for virtual fields
92
- if (requestedFields && requestedFields.length > 0) {
93
- const requestedFieldsList = typeof requestedFields === 'string' ?
94
- requestedFields.split(',').map(f => f.trim()) : requestedFields;
95
-
96
- // Remove virtual fields that weren't requested
97
- Object.entries(schemaStructure).forEach(([fieldName, fieldDef]) => {
98
- if (fieldDef.virtual === true && fieldName in filteredAttributes) {
99
- if (!requestedFieldsList.includes(fieldName)) {
100
- delete filteredAttributes[fieldName];
101
- }
102
- }
103
- });
60
+ // Apply getter (can be async)
61
+ transformedAttributes[fieldName] = await getterInfo.getter(
62
+ transformedAttributes[fieldName],
63
+ getterContext
64
+ )
65
+ } catch (error) {
66
+ console.error(`Error in getter for field '${fieldName}' in ${scopeName}:`, error)
67
+ // Keep current value on error (don't break the whole request)
104
68
  }
69
+ }
70
+ }
105
71
 
106
- // Determine which computed fields to calculate
107
- // We only compute fields that are requested to optimize performance
108
- let fieldsToCompute = [];
109
- if (requestedComputedFields) {
110
- // Explicit list provided (from sparse fieldsets)
111
- // Example: ?fields[products]=name,profit_margin -> only compute profit_margin
112
- fieldsToCompute = requestedComputedFields;
113
- } else if (isMainResource || !parentContext?.queryParams?.fields) {
114
- // No sparse fieldsets or this is the main resource - compute all fields
115
- // This ensures all computed fields are available when no filtering is applied
116
- fieldsToCompute = Object.keys(computedFields);
117
- }
72
+ // Filter hidden fields from attributes based on visibility rules
73
+ // This removes hidden:true fields and normallyHidden:true fields (unless requested)
74
+ const requestedFields = parentContext?.queryParams?.fields?.[scopeName]
75
+ const filteredAttributes = filterHiddenFields(transformedAttributes, { structure: schemaStructure }, requestedFields)
118
76
 
119
- // Create compute context with all available resources
120
- // IMPORTANT: We pass the TRANSFORMED attributes (after getters) to compute functions
121
- // This ensures computed fields see the getter-processed values
122
- // Example: if price getter converts string to number, profit_margin sees the number
123
- const computeContext = {
124
- attributes: transformedAttributes, // Transformed attributes after getters
125
- record: { ...transformedAttributes }, // Full record for convenience
126
- context: parentContext,
127
- helpers, // API helpers for complex operations
128
- api, // Full API instance
129
- };
130
-
131
- // Auto-compute fields that have compute functions
132
- for (const fieldName of fieldsToCompute) {
133
- const fieldDef = computedFields[fieldName];
134
- if (fieldDef && fieldDef.compute) {
135
- try {
136
- // Call the compute function with full context
137
- // Example: profit_margin compute gets { attributes: { price: 100, cost: 60 } }
138
- // and returns: ((100 - 60) / 100 * 100) = "40.00"
139
- filteredAttributes[fieldName] = await fieldDef.compute(computeContext);
140
- } catch (error) {
141
- // Log error but don't fail the request - computed fields shouldn't break API
142
- console.error(`Error computing field '${fieldName}' for ${scopeName}:`, error);
143
- filteredAttributes[fieldName] = null;
144
- }
77
+ // Handle virtual fields - they need to be added from parent context if available
78
+ // Virtual fields come from input and need to be preserved in responses
79
+ if (parentContext && parentContext.inputRecord?.data?.attributes) {
80
+ const inputAttrs = parentContext.inputRecord.data.attributes
81
+ Object.entries(schemaStructure).forEach(([fieldName, fieldDef]) => {
82
+ if (fieldDef.virtual === true && fieldName in inputAttrs && inputAttrs[fieldName] !== undefined && inputAttrs[fieldName] !== null) {
83
+ // Add virtual field to filtered attributes if not already there
84
+ if (!(fieldName in filteredAttributes)) {
85
+ filteredAttributes[fieldName] = inputAttrs[fieldName]
145
86
  }
146
87
  }
88
+ })
89
+ }
90
+
91
+ // Now handle sparse fieldsets for virtual fields
92
+ if (requestedFields && requestedFields.length > 0) {
93
+ const requestedFieldsList = typeof requestedFields === 'string'
94
+ ? requestedFields.split(',').map(f => f.trim())
95
+ : requestedFields
147
96
 
148
- // Remove fields that were only fetched as dependencies
149
- // This is the key to the dependency resolution feature:
150
- // 1. We fetched dependencies from DB (e.g., 'cost' for profit_margin)
151
- // 2. We used them in compute functions
152
- // 3. Now we remove them if they weren't explicitly requested
153
- // Example: User requests profit_margin, we fetch cost, compute, then remove cost
154
- const finalAttributes = { ...filteredAttributes };
155
- if (requestedFields && computedDependencies && computedDependencies.length > 0) {
156
- // Parse requested fields if it's a string
157
- const requested = typeof requestedFields === 'string'
158
- ? requestedFields.split(',').map(f => f.trim()).filter(f => f)
159
- : requestedFields;
160
-
161
- for (const dep of computedDependencies) {
162
- // Only remove if it wasn't explicitly requested
163
- // Example: 'cost' is removed unless user explicitly asked for it
164
- if (!requested.includes(dep)) {
165
- delete finalAttributes[dep];
166
- }
97
+ // Remove virtual fields that weren't requested
98
+ Object.entries(schemaStructure).forEach(([fieldName, fieldDef]) => {
99
+ if (fieldDef.virtual === true && fieldName in filteredAttributes) {
100
+ if (!requestedFieldsList.includes(fieldName)) {
101
+ delete filteredAttributes[fieldName]
167
102
  }
168
103
  }
104
+ })
105
+ }
106
+
107
+ // Determine which computed fields to calculate
108
+ // We only compute fields that are requested to optimize performance
109
+ let fieldsToCompute = []
110
+ if (requestedComputedFields) {
111
+ // Explicit list provided (from sparse fieldsets)
112
+ // Example: ?fields[products]=name,profit_margin -> only compute profit_margin
113
+ fieldsToCompute = requestedComputedFields
114
+ } else if (isMainResource || !parentContext?.queryParams?.fields) {
115
+ // No sparse fieldsets or this is the main resource - compute all fields
116
+ // This ensures all computed fields are available when no filtering is applied
117
+ fieldsToCompute = Object.keys(computedFields)
118
+ }
169
119
 
170
- if (requestedFields && requestedFields.length > 0) {
171
- const requestedList = typeof requestedFields === 'string'
172
- ? requestedFields.split(',').map((field) => field.trim()).filter((field) => field)
173
- : requestedFields;
174
- const allowed = new Set(requestedList);
175
- Object.keys(finalAttributes).forEach((key) => {
176
- if (!allowed.has(key)) {
177
- delete finalAttributes[key];
178
- }
179
- });
120
+ // Create compute context with all available resources
121
+ // IMPORTANT: We pass the TRANSFORMED attributes (after getters) to compute functions
122
+ // This ensures computed fields see the getter-processed values
123
+ // Example: if price getter converts string to number, profit_margin sees the number
124
+ const computeContext = {
125
+ attributes: transformedAttributes, // Transformed attributes after getters
126
+ record: { ...transformedAttributes }, // Full record for convenience
127
+ context: parentContext,
128
+ helpers, // API helpers for complex operations
129
+ api, // Full API instance
130
+ }
131
+
132
+ // Auto-compute fields that have compute functions
133
+ for (const fieldName of fieldsToCompute) {
134
+ const fieldDef = computedFields[fieldName]
135
+ if (fieldDef && fieldDef.compute) {
136
+ try {
137
+ // Call the compute function with full context
138
+ // Example: profit_margin compute gets { attributes: { price: 100, cost: 60 } }
139
+ // and returns: ((100 - 60) / 100 * 100) = "40.00"
140
+ filteredAttributes[fieldName] = await fieldDef.compute(computeContext)
141
+ } catch (error) {
142
+ // Log error but don't fail the request - computed fields shouldn't break API
143
+ console.error(`Error computing field '${fieldName}' for ${scopeName}:`, error)
144
+ filteredAttributes[fieldName] = null
180
145
  }
146
+ }
147
+ }
148
+
149
+ // Remove fields that were only fetched as dependencies
150
+ // This is the key to the dependency resolution feature:
151
+ // 1. We fetched dependencies from DB (e.g., 'cost' for profit_margin)
152
+ // 2. We used them in compute functions
153
+ // 3. Now we remove them if they weren't explicitly requested
154
+ // Example: User requests profit_margin, we fetch cost, compute, then remove cost
155
+ const finalAttributes = { ...filteredAttributes }
156
+ if (requestedFields && computedDependencies && computedDependencies.length > 0) {
157
+ // Parse requested fields if it's a string
158
+ const requested = typeof requestedFields === 'string'
159
+ ? requestedFields.split(',').map(f => f.trim()).filter(f => f)
160
+ : requestedFields
161
+
162
+ for (const dep of computedDependencies) {
163
+ // Only remove if it wasn't explicitly requested
164
+ // Example: 'cost' is removed unless user explicitly asked for it
165
+ if (!requested.includes(dep)) {
166
+ delete finalAttributes[dep]
167
+ }
168
+ }
169
+ }
170
+
171
+ if (requestedFields && requestedFields.length > 0) {
172
+ const requestedList = typeof requestedFields === 'string'
173
+ ? requestedFields.split(',').map((field) => field.trim()).filter((field) => field)
174
+ : requestedFields
175
+ const allowed = new Set(requestedList)
176
+ Object.keys(finalAttributes).forEach((key) => {
177
+ if (!allowed.has(key)) {
178
+ delete finalAttributes[key]
179
+ }
180
+ })
181
+ }
182
+
183
+ // Create context for enrichAttributes hooks
184
+ Object.assign(context, {
185
+ parentContext,
186
+ attributes: finalAttributes, // Use the final attributes after dependency removal
187
+ computedFields,
188
+ requestedComputedFields: fieldsToCompute,
189
+ scopeName,
190
+ helpers,
191
+ api
192
+ })
193
+
194
+ // Run enrichAttributes hooks for additional/override computations
195
+ await runHooks('enrichAttributes')
181
196
 
182
- // Create context for enrichAttributes hooks
183
- Object.assign(context, {
184
- parentContext,
185
- attributes: finalAttributes, // Use the final attributes after dependency removal
186
- computedFields,
187
- requestedComputedFields: fieldsToCompute,
188
- scopeName,
189
- helpers,
190
- api
191
- });
192
-
193
- // Run enrichAttributes hooks for additional/override computations
194
- await runHooks('enrichAttributes');
195
-
196
- // Return the attributes from context, which hooks may have modified
197
- return context.attributes;
198
- };
197
+ // Return the attributes from context, which hooks may have modified
198
+ return context.attributes
199
+ };