     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	  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
    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())
    95	      : requestedFields
    96	
    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]
   102	        }
   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	  }
   119	
   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
   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')
   196	
   197	  // Return the attributes from context, which hooks may have modified
   198	  return context.attributes
   199	};
