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.
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 +90 -32
  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,115 @@
1
+
2
+ import { RestApiResourceError } from "../../../lib/rest-api-errors.js";
3
+ import { findRelationshipDefinition } from "./common.js";
4
+
5
+ /**
6
+ * DELETE RELATIONSHIP
7
+ * Removes specific members from a to-many relationship
8
+ * DELETE /api/articles/1/relationships/tags
9
+ *
10
+ * @param {string} id - The ID of the resource
11
+ * @param {string} relationshipName - The name of the relationship
12
+ * @param {array} relationshipData - Array of resource identifiers to remove
13
+ * @returns {Promise<void>} 204 No Content
14
+ */
15
+ export default async function deleteRelationshipMethod({ params, context, vars, helpers, scope, scopes, runHooks, scopeName, api }) {
16
+ context.method = 'deleteRelationship';
17
+ context.id = params.id;
18
+ context.relationshipName = params.relationshipName;
19
+ context.schemaInfo = scopes[scopeName].vars.schemaInfo;
20
+
21
+ // Transaction handling
22
+ context.transaction = params.transaction ||
23
+ (helpers.newTransaction && !params.transaction ? await helpers.newTransaction() : null);
24
+ context.shouldCommit = !params.transaction && !!context.transaction;
25
+ context.db = context.transaction || api.knex.instance
26
+
27
+ try {
28
+ // Validate
29
+ const relDef = findRelationshipDefinition(context.schemaInfo, context.relationshipName);
30
+ if (!relDef) {
31
+ throw new RestApiResourceError(
32
+ `Relationship '${context.relationshipName}' not found on resource '${scopeName}'`,
33
+ { subtype: 'relationship_not_found' }
34
+ );
35
+ }
36
+
37
+ if (!relDef.hasMany && !relDef.manyToMany) {
38
+ throw new RestApiValidationError(
39
+ `Cannot DELETE from to-one relationship '${context.relationshipName}'`,
40
+ { fields: ['data'] }
41
+ );
42
+ }
43
+
44
+ if (!Array.isArray(params.relationshipData)) {
45
+ throw new RestApiPayloadError('DELETE from relationship requires array of resource identifiers');
46
+ }
47
+
48
+ // Check permissions
49
+ await runHooks('checkPermissions');
50
+ await runHooks('checkPermissionsDeleteRelationship');
51
+
52
+ // Verify parent exists
53
+ const exists = await helpers.dataExists({
54
+ scopeName,
55
+ context: { db: context.db, id: context.id, schemaInfo: context.schemaInfo }
56
+ });
57
+
58
+ if (!exists) {
59
+ throw new RestApiResourceError('Resource not found', { subtype: 'not_found' });
60
+ }
61
+
62
+ // Remove relationships
63
+ if (relDef.manyToMany || (relDef.hasMany && relDef.through)) {
64
+ const manyToManyDef = relDef.manyToMany || {
65
+ through: relDef.through,
66
+ foreignKey: relDef.foreignKey,
67
+ otherKey: relDef.otherKey
68
+ };
69
+
70
+ const knex = api.knex?.instance || helpers.db;
71
+ const pivotResource = manyToManyDef.through;
72
+ const pivotScope = api.resources[pivotResource];
73
+ const pivotTable = pivotScope?.vars?.schemaInfo?.tableName || pivotResource;
74
+ const localKey = manyToManyDef.foreignKey;
75
+ const foreignKey = manyToManyDef.otherKey;
76
+
77
+ for (const identifier of params.relationshipData) {
78
+ await knex(pivotTable)
79
+ .where(localKey, context.id)
80
+ .where(foreignKey, identifier.id)
81
+ .delete()
82
+ .transacting(context.transaction);
83
+ }
84
+ } else {
85
+ // Null out foreign keys for hasMany
86
+ const targetType = relDef.hasMany;
87
+ for (const identifier of params.relationshipData) {
88
+ await api.resources[targetType].patch({
89
+ id: identifier.id,
90
+ inputRecord: {
91
+ data: {
92
+ type: targetType,
93
+ id: identifier.id,
94
+ attributes: { [relDef.foreignKey]: null }
95
+ }
96
+ },
97
+ transaction: context.transaction,
98
+ simplified: false
99
+ });
100
+ }
101
+ }
102
+
103
+ await runHooks('finish');
104
+ await runHooks('finishDeleteRelationship');
105
+
106
+ if (context.shouldCommit) {
107
+ await context.transaction.commit();
108
+ }
109
+
110
+ return; // 204 No Content
111
+
112
+ } catch (error) {
113
+ await handleWriteMethodError(error, context, 'DELETE_RELATIONSHIP', scopeName, log);
114
+ }
115
+ }
@@ -0,0 +1,109 @@
1
+ import { RestApiResourceError } from '../../../lib/rest-api-errors.js';
2
+ import { handleWriteMethodError } from './common.js';
3
+
4
+ /**
5
+ * DELETE
6
+ * Permanently deletes a resource.
7
+ * Returns 204 No Content on success, indicating the resource has been removed.
8
+ * This method does not return the deleted resource.
9
+ */
10
+ export default async function deleteMethod({
11
+ params,
12
+ context,
13
+ vars,
14
+ helpers,
15
+ scope,
16
+ scopes,
17
+ runHooks,
18
+ apiOptions,
19
+ pluginOptions,
20
+ scopeOptions,
21
+ scopeName,
22
+ api,
23
+ log
24
+ }) {
25
+ // Make the method available to all hooks
26
+ context.method = 'delete';
27
+
28
+ // Set the ID in context
29
+ context.id = params.id;
30
+
31
+ // Set scopeName in context (needed for broadcasting)
32
+ context.scopeName = scopeName;
33
+
34
+ // Set schema info even for DELETE (needed by storage layer)
35
+ context.schemaInfo = scopes[scopeName].vars.schemaInfo;
36
+
37
+ // Transaction handling
38
+ context.transaction = params.transaction ||
39
+ (helpers.newTransaction && !params.transaction ? await helpers.newTransaction() : null);
40
+ context.shouldCommit = !params.transaction && !!context.transaction;
41
+ context.db = context.transaction || api.knex.instance;
42
+
43
+ try {
44
+ // No payload validation needed for DELETE
45
+
46
+ // Fetch minimal record for authorization and logging
47
+ const minimalRecord = await helpers.dataGetMinimal({
48
+ scopeName,
49
+ context,
50
+ runHooks
51
+ });
52
+
53
+ if (!minimalRecord) {
54
+ throw new RestApiResourceError(
55
+ `Resource not found`,
56
+ {
57
+ subtype: 'not_found',
58
+ resourceType: scopeName,
59
+ resourceId: context.id
60
+ }
61
+ );
62
+ }
63
+
64
+ context.originalMinimalRecord = minimalRecord;
65
+ context.minimalRecord = minimalRecord;
66
+
67
+ // Centralised checkPermissions function
68
+ await scope.checkPermissions({
69
+ method: 'delete',
70
+ auth: context.auth,
71
+ id: context.id,
72
+ minimalRecord: context.minimalRecord,
73
+ transaction: context.transaction
74
+ });
75
+
76
+ // Before data operations
77
+ await runHooks('beforeDataCall');
78
+ await runHooks('beforeDataCallDelete');
79
+
80
+ // Initialize record context for hooks
81
+ context.record = {};
82
+
83
+ // Call the storage helper
84
+ await helpers.dataDelete({
85
+ scopeName,
86
+ context
87
+ });
88
+
89
+ await runHooks('afterDataCallDelete');
90
+ await runHooks('afterDataCall');
91
+
92
+ // No return record for DELETE (204 No Content)
93
+
94
+ await runHooks('finish');
95
+ await runHooks('finishDelete');
96
+
97
+ // Commit transaction if we created it
98
+ if (context.shouldCommit) {
99
+ await context.transaction.commit();
100
+ await runHooks('afterCommit');
101
+ }
102
+
103
+ // DELETE typically returns void/undefined (204 No Content)
104
+ return;
105
+
106
+ } catch (error) {
107
+ await handleWriteMethodError(error, context, 'DELETE', scopeName, log, runHooks);
108
+ }
109
+ }
@@ -0,0 +1,186 @@
1
+ import { filterHiddenFields } from '../lib/querying-writing/field-utils.js';
2
+
3
+ /**
4
+ * enrichAttributes
5
+ * Runs the enrichAttributes hook for a specific scope to allow plugins to modify attributes
6
+ * before they are returned to the client. This is a scope method so each resource type
7
+ * can have its own attribute enrichment logic.
8
+ *
9
+ *
10
+ */
11
+ export default async function enrichAttributesMethod({ context, params, runHooks, scopeName, scopes, api, helpers }) {
12
+ // Extract parameters passed to enrichAttributes
13
+ // - attributes: The raw attributes from database
14
+ // - parentContext: The context from the calling method (has queryParams, transaction, etc.)
15
+ // - requestedComputedFields: Which computed fields to calculate (from sparse fieldsets)
16
+ // - isMainResource: Whether this is the main resource or an included one
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
+ }
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 || [];
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
+ }
69
+ }
70
+ }
71
+
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
+ });
104
+ }
105
+
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
+ }
118
+
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
+ }
145
+ }
146
+ }
147
+
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
+ }
167
+ }
168
+ }
169
+
170
+ // Create context for enrichAttributes hooks
171
+ Object.assign(context, {
172
+ parentContext,
173
+ attributes: finalAttributes, // Use the final attributes after dependency removal
174
+ computedFields,
175
+ requestedComputedFields: fieldsToCompute,
176
+ scopeName,
177
+ helpers,
178
+ api
179
+ });
180
+
181
+ // Run enrichAttributes hooks for additional/override computations
182
+ await runHooks('enrichAttributes');
183
+
184
+ // Return the attributes from context, which hooks may have modified
185
+ return context.attributes;
186
+ };