     1	import { RestApiResourceError } from '../../../lib/rest-api-errors.js'
     2	import { findRelationshipDefinition } from './common.js'
     3	import { buildRelationshipUrl } from '../lib/querying/url-helpers.js'
     4	
     5	/**
     6	 * GET RELATED
     7	 * Retrieves the actual related resources (full data)
     8	 * GET /api/articles/1/comments
     9	 *
    10	 * @param {string} id - The ID of the parent resource
    11	 * @param {string} relationshipName - The name of the relationship
    12	 * @param {object} queryParams - Standard query parameters
    13	 * @returns {Promise<object>} Related resources with full data
    14	 */
    15	export default async function getRelatedMethod ({ params, context, vars, helpers, scope, scopes, runHooks, scopeName, api }) {
    16	  context.method = 'getRelated'
    17	  context.id = params.id
    18	  context.relationshipName = params.relationshipName
    19	  context.queryParams = params.queryParams || {}
    20	  context.schemaInfo = scopes[scopeName].vars.schemaInfo
    21	  context.transaction = params.transaction
    22	  context.db = context.transaction || api.knex.instance
    23	
    24	  // Validate the relationship exists
    25	  const relDef = findRelationshipDefinition(context.schemaInfo, context.relationshipName)
    26	
    27	  if (!relDef) {
    28	    throw new RestApiResourceError(
    29	      `Relationship '${context.relationshipName}' not found on resource '${scopeName}'`,
    30	      { subtype: 'relationship_not_found' }
    31	    )
    32	  }
    33	
    34	  // Determine target type based on relationship type
    35	  let targetType
    36	  if (relDef.type === 'hasMany' || relDef.type === 'hasOne') {
    37	    targetType = relDef.target
    38	  } else if (relDef.type === 'manyToMany') {
    39	    targetType = context.relationshipName // For manyToMany, use the relationship name
    40	  } else if (relDef.belongsTo) {
    41	    targetType = relDef.belongsTo // belongsTo still in schema
    42	  }
    43	
    44	  if (!targetType || !scopes[targetType]) {
    45	    throw new RestApiResourceError(
    46	      `Related resource type '${targetType}' not found`,
    47	      { subtype: 'related_type_not_found' }
    48	    )
    49	  }
    50	
    51	  // Check permissions
    52	  await runHooks('checkPermissions')
    53	  await runHooks('checkPermissionsGetRelated')
    54	
    55	  // Verify parent exists
    56	  const exists = await helpers.dataExists({
    57	    scopeName,
    58	    context: { db: context.db, id: context.id, schemaInfo: context.schemaInfo }
    59	  })
    60	
    61	  if (!exists) {
    62	    throw new RestApiResourceError('Resource not found', { subtype: 'not_found' })
    63	  }
    64	
    65	  // Handle to-one relationships (belongsTo and hasOne)
    66	  // For example: GET /api/books/1/country or GET /api/books/1/publisher
    67	  if (relDef.belongsTo || relDef.type === 'hasOne') {
    68	    // OPTIMIZATION: Detect if we actually need to make two API calls
    69	    //
    70	    // The naive approach always makes 2 calls:
    71	    // 1. Get parent with relationship included (fetches FULL related record)
    72	    // 2. Extract just the ID and fetch the same record again with queryParams
    73	    //
    74	    // This optimization checks if there are queryParams that would affect
    75	    // the related resource. If not, we can use the data from the first call.
    76	    const hasRelevantQueryParams = context.queryParams && (
    77	      // Check for includes on the related resource (e.g., ?include=some.nested.relation)
    78	      context.queryParams.include?.length > 0 ||
    79	      // Check for field selection on the related resource (e.g., ?fields[countries]=name,code)
    80	      context.queryParams.fields?.[targetType] ||
    81	      // Note: Filters and sorting don't make sense for a single to-one relationship
    82	      // so we don't check for them
    83	      false
    84	    )
    85	
    86	    if (hasRelevantQueryParams) {
    87	      // CASE 1: Has queryParams that affect the related resource
    88	      // We need to make two calls to properly apply the queryParams
    89	
    90	      // First call: Get parent with minimal data (just need the related ID)
    91	      const parent = await scope.get({
    92	        id: context.id,
    93	        queryParams: {
    94	          include: [context.relationshipName],
    95	          fields: { [scopeName]: vars.idProperty || 'id' } // Only fetch parent ID to minimize data
    96	        },
    97	        transaction: context.transaction,
    98	        simplified: false,
    99	        isTransport: params.isTransport
   100	      })
   101	
   102	      const relatedId = parent.data.relationships?.[context.relationshipName]?.data?.id
   103	      if (!relatedId) {
   104	        return {
   105	          links: { self: buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false) },
   106	          data: null
   107	        }
   108	      }
   109	
   110	      // Second call: Get the related resource with all queryParams applied
   111	      const related = await api.resources[targetType].get({
   112	        id: relatedId,
   113	        queryParams: context.queryParams,
   114	        transaction: context.transaction,
   115	        simplified: false,
   116	        isTransport: params.isTransport
   117	      })
   118	
   119	      return {
   120	        links: { self: buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false) },
   121	        data: related.data,
   122	        included: related.included
   123	      }
   124	    } else {
   125	      // CASE 2: No queryParams that affect the related resource
   126	      // We can get everything in one call and extract from included
   127	
   128	      // Single call: Get parent with full related resource included
   129	      const parent = await scope.get({
   130	        id: context.id,
   131	        queryParams: {
   132	          include: [context.relationshipName],
   133	          fields: context.queryParams.fields // Respect any field selections for the parent
   134	        },
   135	        transaction: context.transaction,
   136	        simplified: false,
   137	        isTransport: params.isTransport
   138	      })
   139	
   140	      // Extract the related resource from the parent's relationships
   141	      const relatedId = parent.data.relationships?.[context.relationshipName]?.data?.id
   142	      if (!relatedId) {
   143	        return {
   144	          links: { self: buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false) },
   145	          data: null
   146	        }
   147	      }
   148	
   149	      // Find the full related resource in the included array
   150	      // The include system already fetched it for us!
   151	      const relatedResource = parent.included?.find(
   152	        r => r.type === targetType && r.id === relatedId
   153	      )
   154	
   155	      return {
   156	        links: { self: buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false) },
   157	        data: relatedResource || null
   158	      }
   159	    }
   160	  }
   161	
   162	  // Handle simple hasMany (one-to-many, NOT many-to-many)
   163	  if (relDef.type === 'hasMany') {
   164	    // Check if this is a polymorphic relationship using 'via'
   165	    if (relDef.via) {
   166	      // Polymorphic hasMany relationship
   167	      // Example: publishers hasMany reviews via reviewable
   168	      const targetRelationships = scopes[targetType].vars.schemaInfo.schemaRelationships
   169	      const viaRel = targetRelationships?.[relDef.via]
   170	
   171	      if (!viaRel?.belongsToPolymorphic) {
   172	        throw new RestApiResourceError(
   173	          `Via relationship '${relDef.via}' not found or not polymorphic in '${targetType}'`,
   174	          { subtype: 'invalid_via_relationship' }
   175	        )
   176	      }
   177	
   178	      const { typeField, idField } = viaRel.belongsToPolymorphic
   179	
   180	      // Add polymorphic filters
   181	      const filters = {
   182	        ...context.queryParams.filters,
   183	        [typeField]: scopeName,
   184	        [idField]: context.id
   185	      }
   186	
   187	      const result = await api.resources[targetType].query({
   188	        queryParams: { ...context.queryParams, filters },
   189	        transaction: context.transaction,
   190	        simplified: false,
   191	        isTransport: params.isTransport
   192	      })
   193	
   194	      if (!result.links) {
   195	        result.links = {}
   196	      }
   197	      result.links.self = buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false)
   198	      return result
   199	    } else {
   200	      // Regular hasMany with foreignKey
   201	      // Need to find the relationship name in the target resource that points back to this resource
   202	      const targetSchema = scopes[targetType].vars.schemaInfo.schemaStructure
   203	      let relationshipFilterName = null
   204	
   205	      // Find the field in target schema that has the foreign key and get its relationship name
   206	      for (const [fieldName, fieldDef] of Object.entries(targetSchema)) {
   207	        if (fieldName === relDef.foreignKey && fieldDef.belongsTo === scopeName && fieldDef.as) {
   208	          relationshipFilterName = fieldDef.as
   209	          break
   210	        }
   211	      }
   212	
   213	      // Fall back to foreign key if no relationship name found (shouldn't happen with proper schema)
   214	      const filterKey = relationshipFilterName || relDef.foreignKey
   215	
   216	      const filters = {
   217	        ...context.queryParams.filters,
   218	        [filterKey]: context.id
   219	      }
   220	
   221	      const result = await api.resources[targetType].query({
   222	        queryParams: { ...context.queryParams, filters },
   223	        transaction: context.transaction,
   224	        simplified: false,
   225	        isTransport: params.isTransport
   226	      })
   227	
   228	      if (!result.links) {
   229	        result.links = {}
   230	      }
   231	      result.links.self = buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false)
   232	      return result
   233	    }
   234	  }
   235	
   236	  // Handle many-to-many relationships
   237	  // For example: GET /api/authors/1/books (where authors and books are linked via book_authors)
   238	  if (relDef?.through) {
   239	    if (api.anyapi?.links?.listMany) {
   240	      const identifiers = await api.anyapi.links.listMany({
   241	        context,
   242	        scopeName,
   243	        relName: context.relationshipName,
   244	      })
   245	
   246	      const results = []
   247	      for (const identifier of identifiers) {
   248	        const related = await api.resources[targetType].get({
   249	          id: identifier.id,
   250	          queryParams: context.queryParams,
   251	          transaction: context.transaction,
   252	          simplified: false,
   253	          isTransport: params.isTransport,
   254	        })
   255	        if (related?.data) {
   256	          results.push(related.data)
   257	        }
   258	      }
   259	
   260	      return {
   261	        links: {
   262	          self: buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false)
   263	        },
   264	        data: results,
   265	      }
   266	    }
   267	
   268	    // Legacy fallback
   269	    const pivotResource = relDef.through
   270	    const foreignKey = relDef.foreignKey
   271	    const otherKey = relDef.otherKey
   272	    if (!foreignKey || !otherKey) {
   273	      throw new Error('Missing foreignKey or otherKey in many-to-many relationship')
   274	    }
   275	    const pivotScope = scopes[pivotResource]
   276	    if (!pivotScope) {
   277	      throw new RestApiResourceError(
   278	        `Pivot table resource '${pivotResource}' not found`,
   279	        { subtype: 'pivot_table_not_found' }
   280	      )
   281	    }
   282	    const pivotSchema = pivotScope.vars.schemaInfo.schemaStructure
   283	    let parentRelationshipName = null
   284	    for (const [fieldName, fieldDef] of Object.entries(pivotSchema)) {
   285	      if (fieldName === foreignKey && fieldDef.belongsTo === scopeName && fieldDef.as) {
   286	        parentRelationshipName = fieldDef.as
   287	        break
   288	      }
   289	    }
   290	    const filterKey = parentRelationshipName || foreignKey
   291	    const pivotFilters = {
   292	      [filterKey]: context.id
   293	    }
   294	    const pivotResult = await api.resources[pivotResource].query({
   295	      queryParams: {
   296	        filters: pivotFilters,
   297	        include: [context.relationshipName],
   298	        fields: context.queryParams.fields,
   299	        sort: context.queryParams.sort,
   300	        page: context.queryParams.page
   301	      },
   302	      transaction: context.transaction,
   303	      simplified: false,
   304	      isTransport: params.isTransport
   305	    })
   306	    let includedResources = pivotResult.included?.filter((r) => r.type === targetType) || []
   307	
   308	    if (includedResources.length === 0) {
   309	      const pivotData = pivotResult.data || []
   310	      const relatedIds = [...new Set(pivotData
   311	        .map((item) => {
   312	          const attrId = item?.attributes?.[otherKey]
   313	          if (attrId !== null && attrId !== undefined) {
   314	            return String(attrId)
   315	          }
   316	          const relationships = item?.relationships || {}
   317	          for (const rel of Object.values(relationships)) {
   318	            const data = rel?.data
   319	            if (data?.type === targetType && data?.id != null) {
   320	              return String(data.id)
   321	            }
   322	          }
   323	          return null
   324	        })
   325	        .filter((id) => id !== null)
   326	      )]
   327	
   328	      if (relatedIds.length > 0) {
   329	        includedResources = []
   330	        for (const relatedId of relatedIds) {
   331	          const related = await api.resources[targetType].get({
   332	            id: relatedId,
   333	            queryParams: context.queryParams,
   334	            transaction: context.transaction,
   335	            simplified: false,
   336	            isTransport: params.isTransport,
   337	          })
   338	          if (related?.data) {
   339	            includedResources.push(related.data)
   340	          }
   341	        }
   342	      }
   343	    }
   344	
   345	    return {
   346	      links: {
   347	        self: buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false)
   348	      },
   349	      data: includedResources,
   350	      meta: pivotResult.meta
   351	    }
   352	  }
   353	}
