     1	import {
     2	  RestApiValidationError,
     3	  RestApiPayloadError
     4	} from '../../../lib/rest-api-errors.js'
     5	import { transformSimplifiedToJsonApi } from '../lib/querying-writing/simplified-helpers.js'
     6	import { createEnhancedLogger } from '../../../lib/enhanced-logger.js'
     7	
     8	/**
     9	 * Gets an enhanced logger instance with full error details and stack traces
    10	 * @param {Object} log - The base logger instance
    11	 * @returns {Object} Enhanced logger instance
    12	 */
    13	export const getEnhancedLogger = (log) => {
    14	  return createEnhancedLogger(log, {
    15	    logFullErrors: true,
    16	    includeStack: true
    17	  })
    18	}
    19	
    20	/**
    21	 * Cascades configuration values through multiple sources with fallback to default
    22	 * @param {string} settingName - The setting name to look for
    23	 * @param {Array} sources - Array of objects to search through
    24	 * @param {*} defaultValue - Default value if not found
    25	 * @returns {*} The first defined value found or the default
    26	 */
    27	export const cascadeConfig = (settingName, sources, defaultValue) =>
    28	  sources.find(source => source?.[settingName] !== undefined)?.[settingName] ?? defaultValue
    29	
    30	/**
    31	 * Normalizes return record settings to valid values
    32	 * @param {string|boolean} value - The value to normalize
    33	 * @returns {string} Normalized value: 'no', 'minimal', or 'full'
    34	 */
    35	export function normalizeReturnValue (value) {
    36	  if (['no', 'minimal', 'full'].includes(value)) return value
    37	  return 'no' // default
    38	}
    39	
    40	/**
    41	 * Sets up common request context for REST API methods
    42	 * Handles simplified mode, transaction setup, and initial validation
    43	 *
    44	 * @param {Object} params - The method parameters
    45	 * @param {Object} context - The request context
    46	 * @param {Object} vars - Plugin variables
    47	 * @param {Object} scopes - All available scopes
    48	 * @param {Object} scopeOptions - Scope-specific options
    49	 * @param {string} scopeName - The name of the current scope
    50	 * @param {Object} api - The API instance
    51	 * @param {Object} helpers - Helper functions
    52	 * @returns {Object} An object containing schema-related shortcuts
    53	 */
    54	export async function setupCommonRequest ({ params, context, vars, scopes, scopeOptions, scopeName, api, helpers }) {
    55	  // Determine which simplified setting to use based on transport
    56	  const isTransport = params.isTransport === true
    57	
    58	  // Use vars which automatically cascade from scope to global
    59	  const defaultSimplified = isTransport ? vars.simplifiedTransport : vars.simplifiedApi
    60	
    61	  // Get simplified setting - from params only (per-call override) or use default
    62	  context.simplified = params.simplified !== undefined ? params.simplified : defaultSimplified
    63	
    64	  // Special case: if no inputRecord provided, force simplified mode
    65	  if (!params.inputRecord && context.simplified === false) {
    66	    context.simplified = true
    67	  }
    68	
    69	  // Params is totally bypassed in simplified mode
    70	  if (context.simplified) {
    71	    if (params.inputRecord) {
    72	      context.inputRecord = params.inputRecord
    73	      context.params = params
    74	    } else {
    75	      context.inputRecord = params
    76	      // Preserve returnFullRecord if specified in params
    77	      context.params = params.returnFullRecord ? { returnFullRecord: params.returnFullRecord } : {}
    78	    }
    79	  } else {
    80	    context.inputRecord = params.inputRecord
    81	    context.params = params
    82	  }
    83	
    84	  // Assign common context properties
    85	  context.schemaInfo = scopes[scopeName].vars.schemaInfo
    86	
    87	  // Use vars which automatically cascade from scope to global
    88	  const defaultReturnFullRecord = isTransport ? vars.returnRecordTransport : vars.returnRecordApi
    89	
    90	  // Get return record setting - from params only (per-call override) or use default
    91	  const returnFullRecordRaw = context.params.returnFullRecord !== undefined
    92	    ? context.params.returnFullRecord
    93	    : defaultReturnFullRecord
    94	
    95	  // Normalize return record setting to always be an object with method keys
    96	  if (typeof returnFullRecordRaw === 'object' && returnFullRecordRaw !== null) {
    97	    // It's already an object, normalize the values
    98	    context.returnRecordSetting = {
    99	      post: normalizeReturnValue(returnFullRecordRaw.post),
   100	      put: normalizeReturnValue(returnFullRecordRaw.put),
   101	      patch: normalizeReturnValue(returnFullRecordRaw.patch)
   102	    }
   103	  } else {
   104	    // It's a single value (string or boolean), apply to all methods
   105	    const normalized = normalizeReturnValue(returnFullRecordRaw)
   106	    context.returnRecordSetting = {
   107	      post: normalized,
   108	      put: normalized,
   109	      patch: normalized
   110	    }
   111	  }
   112	
   113	  // Helper function to normalize return values (same as above)
   114	  function normalizeReturnValue (value) {
   115	    if (['no', 'minimal', 'full'].includes(value)) return value
   116	    return 'no' // default
   117	  }
   118	
   119	  // These only make sense as parameter per query, not in vars etc.
   120	  context.queryParams = params.queryParams || {}
   121	  context.queryParams.fields = cascadeConfig('fields', [context.queryParams], {})
   122	  context.queryParams.include = cascadeConfig('include', [context.queryParams], [])
   123	
   124	  context.scopeName = scopeName
   125	
   126	  // Transaction handling
   127	  context.transaction = params.transaction ||
   128	    (helpers.newTransaction && !params.transaction ? await helpers.newTransaction() : null)
   129	  context.shouldCommit = !params.transaction && !!context.transaction
   130	  context.db = context.transaction || api.knex.instance
   131	
   132	  // These are just shortcuts used in this function and will be returned
   133	  const schema = context.schemaInfo.schemaInstance
   134	  const schemaStructure = context.schemaInfo.schemaInstance.structure
   135	  const schemaRelationships = context.schemaInfo.schemaRelationships
   136	
   137	  // Transform input if in simplified mode
   138	  if (context.simplified) {
   139	    context.inputRecord = transformSimplifiedToJsonApi(
   140	      { inputRecord: context.inputRecord },
   141	      { context: { scopeName, schemaStructure, schemaRelationships } }
   142	    )
   143	  } else {
   144	    // Strict mode: validate no belongsTo fields in attributes
   145	    if (context.inputRecord?.data?.attributes) {
   146	      for (const [key, fieldDef] of Object.entries(schemaStructure)) {
   147	        if (fieldDef.belongsTo && key in context.inputRecord.data.attributes) {
   148	          throw new RestApiValidationError(
   149	            `Field '${key}' is a foreign key and must be set via relationships, not attributes`,
   150	            { fields: [`data.attributes.${key}`] }
   151	          )
   152	        }
   153	      }
   154	    }
   155	  }
   156	
   157	  if (context.inputRecord.data.type !== scopeName) {
   158	    throw new RestApiValidationError(
   159	      `Resource type mismatch. Expected '${scopeName}' but got '${context.inputRecord.data.type}'`,
   160	      {
   161	        fields: ['data.type'],
   162	        violations: [{
   163	          field: 'data.type',
   164	          rule: 'resource_type_match',
   165	          message: `Resource type must be '${scopeName}'`
   166	        }]
   167	      }
   168	    )
   169	  }
   170	
   171	  // Remove included validation since JSON:API doesn't support it
   172	  if (context.inputRecord.included) {
   173	    throw new RestApiPayloadError(
   174	      context.method + ' requests cannot include an "included" array. JSON:API does not support creating multiple resources in a single request.',
   175	      { path: 'included', expected: 'undefined', received: 'array' }
   176	    )
   177	  }
   178	
   179	  // If both URL path ID and request body ID are provided, they must match
   180	  // Convert both to strings for comparison since databases may return numeric IDs
   181	  if (context.id && context.inputRecord.data.id && String(context.id) !== String(context.inputRecord.data.id)) {
   182	    throw new RestApiValidationError(
   183	      `ID mismatch. URL path ID '${context.id}' does not match request body ID '${context.inputRecord.data.id}'`,
   184	      {
   185	        fields: ['data.id'],
   186	        violations: [{
   187	          field: 'data.id',
   188	          rule: 'id_consistency',
   189	          message: 'Request body ID must match URL path ID when both are provided'
   190	        }]
   191	      }
   192	    )
   193	  }
   194	
   195	  // Return key schema-related objects for direct use in the main methods
   196	  return { schema, schemaStructure, schemaRelationships }
   197	}
   198	
   199	/**
   200	 * Handles error cleanup and logging for write methods (POST, PUT, PATCH)
   201	 *
   202	 * @param {Error} error - The error that was caught
   203	 * @param {Object} context - The request context
   204	 * @param {string} method - The HTTP method name (POST, PUT, PATCH)
   205	 * @param {string} scopeName - The name of the resource scope
   206	 * @param {Object} log - The logger instance
   207	 * @param {Function} runHooks - Function to run hooks
   208	 * @throws {Error} Re-throws the original error after cleanup
   209	 */
   210	export const handleWriteMethodError = async (error, context, method, scopeName, log, runHooks) => {
   211	  // Rollback transaction if we created it
   212	  if (context.shouldCommit) {
   213	    await context.transaction.rollback()
   214	    await runHooks('afterRollback')
   215	  }
   216	
   217	  // Create enhanced logger for error logging
   218	  const enhancedLog = getEnhancedLogger(log)
   219	
   220	  // Log the full error details
   221	  enhancedLog.logError(`Error in ${method} method`, error, {
   222	    scopeName,
   223	    method: method.toLowerCase(),
   224	    inputRecord: context.inputRecord
   225	  })
   226	
   227	  throw error
   228	}
   229	
   230	/**
   231	 * Validates that a pivot resource exists for many-to-many relationships
   232	 *
   233	 * @param {Object} scopes - All available scopes/resources
   234	 * @param {Object} relDef - The relationship definition
   235	 * @param {string} relName - The relationship name
   236	 * @throws {RestApiValidationError} If the pivot resource doesn't exist
   237	 */
   238	export const validatePivotResource = (scopes, relDef, relName) => {
   239	  if (!scopes[relDef.through]) {
   240	    throw new RestApiValidationError(
   241	      `Pivot resource '${relDef.through}' not found for relationship '${relName}'`,
   242	      {
   243	        fields: [`relationships.${relName}`],
   244	        violations: [{
   245	          field: `relationships.${relName}`,
   246	          rule: 'missing_pivot_resource',
   247	          message: `Pivot resource '${relDef.through}' must be defined`
   248	        }]
   249	      }
   250	    )
   251	  }
   252	}
   253	
   254	/**
   255	 * Gets the appropriate hook suffix based on HTTP method
   256	 *
   257	 * @param {string} method - The HTTP method (e.g., 'post', 'get')
   258	 * @returns {string} The capitalized method name for hook naming
   259	 */
   260	export const getMethodHookSuffix = (method) => {
   261	  return method.charAt(0).toUpperCase() + method.slice(1)
   262	}
   263	
   264	/**
   265	 * Validates resource attributes before write operations
   266	 *
   267	 * @param {Object} params - Validation parameters
   268	 * @param {Object} params.context - The request context
   269	 * @param {Object} params.schema - The resource schema
   270	 * @param {Object} params.belongsToUpdates - BelongsTo relationship updates
   271	 * @param {Function} params.runHooks - Function to run hooks
   272	 * @param {boolean} params.isPartialValidation - Whether this is partial validation (for PATCH)
   273	 * @throws {RestApiValidationError} If validation fails
   274	 */
   275	export const validateResourceAttributesBeforeWrite = async ({
   276	  context,
   277	  schema,
   278	  belongsToUpdates,
   279	  runHooks,
   280	  isPartialValidation = false
   281	}) => {
   282	  // Dynamically get the method suffix
   283	  const methodSpecificHookSuffix = getMethodHookSuffix(context.method)
   284	
   285	  await runHooks('beforeSchemaValidate')
   286	  await runHooks(`beforeSchemaValidate${methodSpecificHookSuffix}`)
   287	
   288	  // Store original input attributes before validation adds defaults (primarily for POST)
   289	  // Only if it's not already set and the current method is POST
   290	  if (!context.originalInputAttributes && context.method === 'post') {
   291	    context.originalInputAttributes = { ...(context.inputRecord.data.attributes || {}) }
   292	  }
   293	
   294	  // Merge belongsTo updates with attributes for validation
   295	  const attributesToValidate = {
   296	    ...context.inputRecord.data.attributes,
   297	    ...belongsToUpdates
   298	  }
   299	
   300	  // Extract computed fields and foreign key fields before validation
   301	  const computedFields = {}
   302	  const foreignKeyFields = {}
   303	  const schemaStructure = context.schemaInfo.schemaStructure || {}
   304	  Object.entries(attributesToValidate).forEach(([key, value]) => {
   305	    const fieldDef = schemaStructure[key]
   306	    // Computed fields should be stripped from input
   307	    if (fieldDef && fieldDef.computed === true && value !== undefined) {
   308	      computedFields[key] = value
   309	    }
   310	    // Foreign key fields (belongsTo with 'as' property) should NOT be in attributes
   311	    // They should only come through relationships or belongsToUpdates
   312	    // Check if key exists in belongsToUpdates (not just truthy value)
   313	    if (fieldDef && fieldDef.belongsTo && fieldDef.as && value !== undefined && !(key in belongsToUpdates)) {
   314	      foreignKeyFields[key] = value
   315	    }
   316	  })
   317	
   318	  // Warn if computed fields were sent in the input
   319	  if (Object.keys(computedFields).length > 0) {
   320	    const fieldNames = Object.keys(computedFields).join(', ')
   321	    console.warn(`Computed fields [${fieldNames}] were sent in input for resource '${context.scopeName}' but will be ignored as they are output-only`)
   322	  }
   323	
   324	  // Reject if foreign key fields were sent directly in attributes
   325	  if (Object.keys(foreignKeyFields).length > 0) {
   326	    const violations = Object.keys(foreignKeyFields).map(field => {
   327	      const fieldDef = schemaStructure[field]
   328	      return {
   329	        field: `data.attributes.${field}`,
   330	        rule: 'foreign_key_in_attributes',
   331	        message: `Foreign key field '${field}' should not be in attributes. Use 'data.relationships.${fieldDef.as}' instead.`
   332	      }
   333	    })
   334	
   335	    throw new RestApiValidationError(
   336	      'Foreign key fields cannot be set directly in attributes',
   337	      {
   338	        fields: violations.map(v => v.field),
   339	        violations
   340	      }
   341	    )
   342	  }
   343	
   344	  // Filter out computed fields and foreign key fields from validation
   345	  // Virtual fields MUST go through validation
   346	  const attributesForValidation = Object.entries(attributesToValidate)
   347	    .filter(([key, _]) => {
   348	      const fieldDef = schemaStructure[key]
   349	      // Exclude computed fields
   350	      if (fieldDef && fieldDef.computed === true) {
   351	        return false
   352	      }
   353	      // Exclude foreign key fields that weren't provided via belongsToUpdates
   354	      if (fieldDef && fieldDef.belongsTo && fieldDef.as && !belongsToUpdates[key]) {
   355	        return false
   356	      }
   357	      return true
   358	    })
   359	    .reduce((acc, [key, value]) => {
   360	      acc[key] = value
   361	      return acc
   362	    }, {})
   363	
   364	  const validationOptions = isPartialValidation ? { onlyObjectValues: true } : {}
   365	
   366	  const { validatedObject, errors } = await schema.validate(attributesForValidation, validationOptions)
   367	
   368	  if (Object.keys(errors).length > 0) {
   369	    // --- START OF MODIFICATION ---
   370	    const schemaStructure = context.schemaInfo.schemaInstance.structure // Get the schema structure for lookup
   371	
   372	    const violations = Object.entries(errors).map(([field, error]) => {
   373	      let fieldPath = `data.attributes.${field}` // Default path for attributes
   374	
   375	      // Check if this field is a foreign key that has an 'as' alias
   376	      const fieldDef = schemaStructure[field]
   377	      if (fieldDef && fieldDef.belongsTo && fieldDef.as) {
   378	      // If it's a belongsTo field with an alias, rewrite the path to the relationship alias
   379	        fieldPath = `data.relationships.${fieldDef.as}.data.id`
   380	      }
   381	      // For many-to-many relationships, the original `transformSimplifiedToJsonApi`
   382	      // already puts them under `relationships.relName.data`, so `field` here
   383	      // would already be the relationship name, not a foreign key.
   384	      // However, if a validation error somehow slips through for a pivot table field
   385	      // that doesn't have an 'as' alias but is a foreign key, you might need
   386	      // more sophisticated mapping. For now, this covers belongsTo.
   387	
   388	      return {
   389	        field: fieldPath,
   390	        rule: error.code || 'invalid_value',
   391	        message: error.message
   392	      }
   393	    })
   394	    // --- END OF MODIFICATION ---
   395	
   396	    throw new RestApiValidationError(
   397	      'Schema validation failed for resource attributes',
   398	      {
   399	        fields: violations.map(v => v.field), // Use the potentially rewritten fields
   400	        violations
   401	      }
   402	    )
   403	  }
   404	
   405	  // Update attributes with validated values
   406	  // Virtual fields have now been validated and cast properly
   407	  context.inputRecord.data.attributes = validatedObject
   408	
   409	  await runHooks(`afterSchemaValidate${methodSpecificHookSuffix}`)
   410	  await runHooks('afterSchemaValidate')
   411	}
   412	
   413	/**
   414	 * Validates that the user has access to all resources referenced in relationships
   415	 *
   416	 * @param {object} context - The context object containing authentication info
   417	 * @param {object} inputRecord - The input record containing relationships to validate
   418	 * @param {object} helpers - Data helpers including dataGetMinimal
   419	 * @param {function} runHooks - Function to run hooks
   420	 * @param {object} api - API instance to access resources
   421	 * @throws {Error} If user doesn't have access to any related resource
   422	 */
   423	export const validateRelationshipAccess = async (context, inputRecord, helpers, runHooks, api) => {
   424	  if (!inputRecord?.data?.relationships) return
   425	
   426	  for (const [relName, relData] of Object.entries(inputRecord.data.relationships)) {
   427	    if (!relData?.data) continue
   428	
   429	    // Handle both single and array relationships
   430	    const relatedItems = Array.isArray(relData.data) ? relData.data : [relData.data]
   431	
   432	    for (const item of relatedItems) {
   433	      // Get the scope for the related resource
   434	      const relatedScope = api.resources[item.type]
   435	      if (!relatedScope) {
   436	        throw new Error(`Unknown resource type: ${item.type}`)
   437	      }
   438	
   439	      // Create context for dataGetMinimal
   440	      const getContext = {
   441	        ...context,
   442	        id: item.id,
   443	        schemaInfo: relatedScope.vars.schemaInfo,
   444	        scopeName: item.type,
   445	        method: 'get', // We're checking read permission
   446	        isUpdate: false
   447	      }
   448	
   449	      // Get the minimal record
   450	      const record = await helpers.dataGetMinimal({
   451	        scopeName: item.type,
   452	        context: getContext,
   453	        runHooks
   454	      })
   455	
   456	      if (!record) {
   457	        throw new Error(`Cannot create relationship to non-existent ${item.type} with id ${item.id}`)
   458	      }
   459	
   460	      // Check permissions using the related scope's checkPermissions
   461	      await relatedScope.checkPermissions({
   462	        method: 'get',
   463	        originalContext: {
   464	          ...context,
   465	          id: item.id,
   466	          minimalRecord: record
   467	        }
   468	      })
   469	    }
   470	  }
   471	}
   472	
   473	/**
   474	 * Applies field setters to transform attribute values before storage
   475	 * Setters are applied in dependency order to handle interdependent fields
   476	 *
   477	 * @param {object} attributes - The validated attributes to transform
   478	 * @param {object} schemaInfo - Schema information including fieldSetters
   479	 * @param {object} context - The request context
   480	 * @param {object} api - The API instance
   481	 * @param {object} helpers - Helper functions
   482	 * @returns {object} Transformed attributes ready for storage
   483	 */
   484	export const applyFieldSetters = async (attributes, schemaInfo, context, api, helpers) => {
   485	  const fieldSetters = schemaInfo.fieldSetters || {}
   486	  const sortedSetterFields = schemaInfo.sortedSetterFields || []
   487	
   488	  // No setters to apply
   489	  if (sortedSetterFields.length === 0) {
   490	    return attributes
   491	  }
   492	
   493	  const transformedAttributes = { ...attributes }
   494	
   495	  for (const fieldName of sortedSetterFields) {
   496	    // Only process fields that exist in the attributes
   497	    if (fieldName in transformedAttributes) {
   498	      const setterInfo = fieldSetters[fieldName]
   499	      try {
   500	        const setterContext = {
   501	          attributes: transformedAttributes, // Current state with previous setters applied
   502	          fieldName,
   503	          originalValue: attributes[fieldName],
   504	          originalAttributes: attributes,
   505	          scopeName: context.scopeName,
   506	          method: context.method,
   507	          api,
   508	          helpers,
   509	          auth: context.auth
   510	        }
   511	        transformedAttributes[fieldName] = await setterInfo.setter(
   512	          transformedAttributes[fieldName],
   513	          setterContext
   514	        )
   515	      } catch (error) {
   516	        console.warn(`Setter for field '${fieldName}' failed:`, error)
   517	        // Keep original value if setter fails
   518	      }
   519	    }
   520	  }
   521	
   522	  return transformedAttributes
   523	}
   524	
   525	export async function handleRecordReturnAfterWrite ({
   526	  context,
   527	  scopeName,
   528	  api,
   529	  scopes,
   530	  schemaStructure,
   531	  schemaRelationships,
   532	  scopeOptions,
   533	  vars,
   534	  runHooks,
   535	  helpers,
   536	  log
   537	}) {
   538	  // Create enhanced logger for warnings
   539	  const enhancedLog = getEnhancedLogger(log)
   540	  const methodSpecificHookSuffix = getMethodHookSuffix(context.method)
   541	
   542	  // Step 1: Set up record state for hooks
   543	  // Handle originalMinimalRecord and minimalRecord based on method type
   544	  if (context.method === 'DELETE') {
   545	    // For DELETE, keep the deleted record reference
   546	    if (context.minimalRecord) {
   547	      context.originalMinimalRecord = context.minimalRecord
   548	    }
   549	  } else {
   550	    // For POST, PUT, PATCH - save the original state if it exists
   551	    if (context.minimalRecord) {
   552	      context.originalMinimalRecord = context.minimalRecord
   553	    }
   554	
   555	    // Fetch the current state of the record after the write operation
   556	    try {
   557	      const currentRecord = await helpers.dataGetMinimal({
   558	        scopeName,
   559	        context,
   560	        runHooks
   561	      })
   562	      context.minimalRecord = currentRecord
   563	    } catch (error) {
   564	      enhancedLog.warn(`Could not fetch minimal record after ${context.method} operation`, { error, id: context.id })
   565	    }
   566	  }
   567	
   568	  // Step 2: Determine what to return based on configuration
   569	  const returnMode = context.returnRecordSetting[context.method]
   570	
   571	  // Case 1: Return nothing (204 No Content)
   572	  if (returnMode === 'no') {
   573	    context.responseRecord = undefined
   574	    await runHooks('finish')
   575	    await runHooks(`finish${methodSpecificHookSuffix}`)
   576	    return undefined
   577	  }
   578	
   579	  // Case 2: Return minimal record (just type and id)
   580	  if (returnMode === 'minimal') {
   581	    if (context.simplified) {
   582	      context.responseRecord = {
   583	        id: String(context.id),
   584	        type: scopeName
   585	      }
   586	    } else {
   587	      context.responseRecord = {
   588	        data: {
   589	          type: scopeName,
   590	          id: String(context.id)
   591	        }
   592	      }
   593	    }
   594	    await runHooks('finish')
   595	    await runHooks(`finish${methodSpecificHookSuffix}`)
   596	    return context.responseRecord
   597	  }
   598	
   599	  // Case 3: Return full record
   600	  if (returnMode === 'full') {
   601	    // Fetch the complete record using the GET method
   602	    const fullRecord = await api.resources[scopeName].get({
   603	      id: context.id,
   604	      queryParams: context.queryParams,
   605	      transaction: context.transaction,
   606	      simplified: context.simplified
   607	    }, { ...context })
   608	
   609	    context.responseRecord = fullRecord || undefined
   610	
   611	    // Run finish hooks
   612	    await runHooks('finish')
   613	    await runHooks(`finish${methodSpecificHookSuffix}`)
   614	
   615	    // No transformation needed - GET already returns the correct format
   616	    // based on context.simplified
   617	    return context.responseRecord
   618	  }
   619	
   620	  // This should never be reached, but just in case
   621	  throw new Error(`Invalid returnMode: ${returnMode}`)
   622	}
   623	
   624	export const findRelationshipDefinition = (schemaInfo, relationshipName) => {
   625	  // First check schemaRelationships (for relationships defined in relationships object)
   626	  const relDef = schemaInfo.schemaRelationships?.[relationshipName]
   627	  if (relDef) {
   628	    return relDef
   629	  }
   630	
   631	  // Then check schema fields for belongsTo relationships with matching 'as' property
   632	  for (const [fieldName, fieldDef] of Object.entries(schemaInfo.schemaStructure || {})) {
   633	    if (fieldDef.as === relationshipName && fieldDef.belongsTo) {
   634	      return fieldDef
   635	    }
   636	  }
   637	
   638	  return null
   639	}
