     1	import { getForeignKeyFields as getForeignKeyFieldsFromUtils } from '../querying-writing/field-utils.js'
     2	import { RELATIONSHIPS_KEY, RELATIONSHIP_METADATA_KEY, ROW_NUMBER_KEY, COMPUTED_DEPENDENCIES_KEY, getSchemaStructure } from '../querying-writing/knex-constants.js'
     3	import { getUrlPrefix, buildResourceUrl, buildRelationshipUrl } from './url-helpers.js'
     4	
     5	/**
     6	 * Transforms a flat database record into JSON:API resource format
     7	 *
     8	 * @private
     9	 * @param {Object} scope - Scope containing schema and configuration
    10	 * @param {Object} record - Database record to transform
    11	 * @param {Object} deps - Dependencies including context with scopeName and polymorphicFields
    12	 * @returns {Object|null} JSON:API resource object or null if no record
    13	 *
    14	 * @example
    15	 * // Input: Database record with foreign keys and internal fields
    16	 * const record = {
    17	 *   id: 1,
    18	 *   title: 'Hello World',
    19	 *   content: 'Article content',
    20	 *   author_id: 2,           // Foreign key
    21	 *   category_id: 5,         // Foreign key
    22	 *   __$jsonrestapi_rn$__: 1 // Internal field
    23	 * };
    24	 *
    25	 * const jsonApiResource = toJsonApi(scope, record, deps);
    26	 *
    27	 * // Output: Clean JSON:API resource
    28	 * // {
    29	 * //   type: 'articles',
    30	 * //   id: '1',
    31	 * //   attributes: {
    32	 * //     title: 'Hello World',
    33	 * //     content: 'Article content'
    34	 * //     // Note: author_id, category_id, and internal fields are removed
    35	 * //   }
    36	 * // }
    37	 *
    38	 * @example
    39	 * // Input: Polymorphic record
    40	 * const record = {
    41	 *   id: 10,
    42	 *   text: 'Great article!',
    43	 *   commentable_type: 'articles',  // Polymorphic type field
    44	 *   commentable_id: 1               // Polymorphic id field
    45	 * };
    46	 * // deps.context.polymorphicFields = Set(['commentable_type', 'commentable_id'])
    47	 *
    48	 * const jsonApiResource = toJsonApi(scope, record, deps);
    49	 *
    50	 * // Output: Polymorphic fields filtered out
    51	 * // {
    52	 * //   type: 'comments',
    53	 * //   id: '10',
    54	 * //   attributes: {
    55	 * //     text: 'Great article!'
    56	 * //   }
    57	 * // }
    58	 *
    59	 * @description
    60	 * Used by:
    61	 * - toJsonApiRecord calls this as the core transformation logic
    62	 * - buildJsonApiResponse uses this indirectly through toJsonApiRecord
    63	 *
    64	 * Purpose:
    65	 * - Separates database structure from API structure
    66	 * - Filters out foreign keys that become relationships in JSON:API
    67	 * - Removes internal fields used for query optimization
    68	 * - Ensures clean attribute objects without implementation details
    69	 *
    70	 * Data flow:
    71	 * - Called after database query returns flat records
    72	 * - Transforms each record individually
    73	 * - Output feeds into relationship building and response assembly
    74	 */
    75	const toJsonApi = (scope, record, deps) => {
    76	  if (!record) return null
    77	
    78	  const schemaInstance = scope.vars.schemaInfo.schemaInstance
    79	
    80	  const { context } = deps
    81	  const scopeName = context.scopeName
    82	  const idProperty = context.schemaInfo?.idProperty || 'id'
    83	  const polymorphicFields = context.polymorphicFields || new Set()
    84	
    85	  const { id, ...allAttributes } = record
    86	
    87	  const foreignKeys = schemaInstance ? getForeignKeyFieldsFromUtils(schemaInstance) : new Set()
    88	
    89	  const internalFields = new Set([
    90	    RELATIONSHIPS_KEY,
    91	    RELATIONSHIP_METADATA_KEY,
    92	    ROW_NUMBER_KEY,
    93	    COMPUTED_DEPENDENCIES_KEY
    94	  ])
    95	
    96	  const attributes = {}
    97	  Object.entries(allAttributes).forEach(([key, value]) => {
    98	    if (!foreignKeys.has(key) && !polymorphicFields.has(key) && !internalFields.has(key) && key !== idProperty) {
    99	      attributes[key] = value
   100	    }
   101	  })
   102	
   103	  return {
   104	    type: scopeName,
   105	    id: String(id),
   106	    attributes
   107	  }
   108	}
   109	
   110	/**
   111	 * Converts a database record to JSON:API format with proper field filtering
   112	 *
   113	 * @param {Object} scope - Scope containing schema and relationship definitions
   114	 * @param {Object} record - Raw database record
   115	 * @param {string} scopeName - Resource type name
   116	 * @returns {Object} JSON:API formatted resource
   117	 *
   118	 * @example
   119	 * // Input: Database record with various field types
   120	 * const record = {
   121	 *   id: 1,
   122	 *   title: 'My Article',
   123	 *   author_id: 2,      // belongsTo relationship
   124	 *   category_id: 3,    // belongsTo relationship
   125	 *   views: 150,
   126	 *   published: true
   127	 * };
   128	 *
   129	 * const jsonApi = toJsonApiRecord(scope, record, 'articles');
   130	 *
   131	 * // Output: Foreign keys filtered out
   132	 * // {
   133	 * //   type: 'articles',
   134	 * //   id: '1',
   135	 * //   attributes: {
   136	 * //     title: 'My Article',
   137	 * //     views: 150,
   138	 * //     published: true
   139	 * //   }
   140	 * // }
   141	 *
   142	 * @example
   143	 * // Input: Record with polymorphic relationship
   144	 * const record = {
   145	 *   id: 5,
   146	 *   body: 'Nice post!',
   147	 *   author_id: 10,
   148	 *   commentable_type: 'posts',    // Polymorphic
   149	 *   commentable_id: 3              // Polymorphic
   150	 * };
   151	 * // Schema has polymorphic relationship defined
   152	 *
   153	 * const jsonApi = toJsonApiRecord(scope, record, 'comments');
   154	 *
   155	 * // Output: Both foreign keys and polymorphic fields filtered
   156	 * // {
   157	 * //   type: 'comments',
   158	 * //   id: '5',
   159	 * //   attributes: {
   160	 * //     body: 'Nice post!'
   161	 * //   }
   162	 * // }
   163	 *
   164	 * @description
   165	 * Used by:
   166	 * - rest-api-knex-plugin's dataGet method for single resource responses
   167	 * - rest-api-knex-plugin's dataQuery method for collection responses
   168	 * - processIncludes when transforming included resources
   169	 * - buildJsonApiResponse as the primary transformation function
   170	 *
   171	 * Purpose:
   172	 * - Provides consistent JSON:API transformation across all query operations
   173	 * - Automatically identifies and filters foreign keys based on schema
   174	 * - Handles polymorphic relationship fields (type/id pairs)
   175	 * - Ensures IDs are always strings as required by JSON:API spec
   176	 *
   177	 * Data flow:
   178	 * 1. Database query returns flat records with all fields
   179	 * 2. toJsonApiRecord identifies foreign keys from schema
   180	 * 3. Filters out foreign keys and internal fields from attributes
   181	 * 4. Returns clean JSON:API resource ready for relationship processing
   182	 * 5. buildJsonApiResponse adds relationships and links to complete the response
   183	 */
   184	export const toJsonApiRecord = (scope, record, scopeName) => {
   185	  const {
   186	    vars: {
   187	      schemaInfo: { schemaRelationships: relationships, idProperty }
   188	    }
   189	  } = scope
   190	
   191	  const polymorphicFields = new Set()
   192	  try {
   193	    Object.entries(relationships || {}).forEach(([relName, relDef]) => {
   194	      if (relDef.belongsToPolymorphic) {
   195	        const typeFieldName = relDef.belongsToPolymorphic.typeField || `${relName}_type`
   196	        const idFieldName = relDef.belongsToPolymorphic.idField || `${relName}_id`
   197	        polymorphicFields.add(typeFieldName)
   198	        polymorphicFields.add(idFieldName)
   199	      }
   200	    })
   201	  } catch (e) {
   202	  }
   203	
   204	  const deps = {
   205	    context: {
   206	      scopeName,
   207	      schemaInfo: { idProperty },
   208	      polymorphicFields
   209	    }
   210	  }
   211	
   212	  return toJsonApi(scope, record, deps)
   213	}
   214	
   215	/**
   216	 * Builds complete JSON:API response with data, relationships, links, and optional includes
   217	 *
   218	 * @async
   219	 * @param {Object} scope - Scope containing schema and configuration
   220	 * @param {Array<Object>} records - Primary records to include in response
   221	 * @param {Array<Object>} included - Resources to include in 'included' array
   222	 * @param {boolean} isSingle - Whether this is a single resource response
   223	 * @param {string} scopeName - Resource type name
   224	 * @param {Object} context - Request context with pagination metadata
   225	 * @returns {Promise<Object>} Complete JSON:API response document
   226	 *
   227	 * @example
   228	 * // Input: Single article with author include
   229	 * const records = [{
   230	 *   id: 1,
   231	 *   title: 'Article Title',
   232	 *   content: 'Article content',
   233	 *   author_id: 2,
   234	 *   category_id: 3
   235	 * }];
   236	 *
   237	 * const included = [{
   238	 *   type: 'authors',
   239	 *   id: '2',
   240	 *   attributes: { name: 'John Doe', email: 'john@example.com' }
   241	 * }];
   242	 *
   243	 * const response = await buildJsonApiResponse(scope, records, included, true, 'articles', context);
   244	 *
   245	 * // Output: Complete JSON:API document
   246	 * // {
   247	 * //   data: {
   248	 * //     type: 'articles',
   249	 * //     id: '1',
   250	 * //     attributes: {
   251	 * //       title: 'Article Title',
   252	 * //       content: 'Article content'
   253	 * //     },
   254	 * //     relationships: {
   255	 * //       author: {
   256	 * //         data: { type: 'authors', id: '2' },
   257	 * //         links: {
   258	 * //           self: '/articles/1/relationships/author',
   259	 * //           related: '/articles/1/author'
   260	 * //         }
   261	 * //       },
   262	 * //       category: {
   263	 * //         data: { type: 'categories', id: '3' },
   264	 * //         links: {
   265	 * //           self: '/articles/1/relationships/category',
   266	 * //           related: '/articles/1/category'
   267	 * //         }
   268	 * //       }
   269	 * //     },
   270	 * //     links: {
   271	 * //       self: '/articles/1'
   272	 * //     }
   273	 * //   },
   274	 * //   included: [{
   275	 * //     type: 'authors',
   276	 * //     id: '2',
   277	 * //     attributes: { name: 'John Doe', email: 'john@example.com' },
   278	 * //     links: { self: '/authors/2' }
   279	 * //   }],
   280	 * //   links: {
   281	 * //     self: '/articles/1'
   282	 * //   }
   283	 * // }
   284	 *
   285	 * @example
   286	 * // Input: Collection with pagination
   287	 * const records = [
   288	 *   { id: 1, title: 'Article 1', author_id: 10 },
   289	 *   { id: 2, title: 'Article 2', author_id: 10 },
   290	 *   { id: 3, title: 'Article 3', author_id: 11 }
   291	 * ];
   292	 *
   293	 * context.returnMeta = {
   294	 *   paginationMeta: { page: 2, pageSize: 3, pageCount: 10, total: 30 },
   295	 *   paginationLinks: {
   296	 *     self: '/articles?page[number]=2&page[size]=3',
   297	 *     first: '/articles?page[number]=1&page[size]=3',
   298	 *     prev: '/articles?page[number]=1&page[size]=3',
   299	 *     next: '/articles?page[number]=3&page[size]=3',
   300	 *     last: '/articles?page[number]=10&page[size]=3'
   301	 *   }
   302	 * };
   303	 *
   304	 * const response = await buildJsonApiResponse(scope, records, [], false, 'articles', context);
   305	 *
   306	 * // Output: Collection with pagination metadata
   307	 * // {
   308	 * //   data: [
   309	 * //     { type: 'articles', id: '1', attributes: { title: 'Article 1' }, ... },
   310	 * //     { type: 'articles', id: '2', attributes: { title: 'Article 2' }, ... },
   311	 * //     { type: 'articles', id: '3', attributes: { title: 'Article 3' }, ... }
   312	 * //   ],
   313	 * //   meta: {
   314	 * //     pagination: { page: 2, pageSize: 3, pageCount: 10, total: 30 }
   315	 * //   },
   316	 * //   links: {
   317	 * //     self: '/articles?page[number]=2&page[size]=3',
   318	 * //     first: '/articles?page[number]=1&page[size]=3',
   319	 * //     prev: '/articles?page[number]=1&page[size]=3',
   320	 * //     next: '/articles?page[number]=3&page[size]=3',
   321	 * //     last: '/articles?page[number]=10&page[size]=3'
   322	 * //   }
   323	 * // }
   324	 *
   325	 * @description
   326	 * Used by:
   327	 * - rest-api-knex-plugin's dataGet method for single resource responses
   328	 * - rest-api-knex-plugin's dataQuery method for collection responses
   329	 * - Called at the end of the query pipeline to assemble the final response
   330	 *
   331	 * Purpose:
   332	 * - Assembles all parts of a JSON:API response in the correct structure
   333	 * - Adds relationship objects with proper links for each foreign key
   334	 * - Handles both regular belongsTo and polymorphic relationships
   335	 * - Adds self links to all resources as required by JSON:API
   336	 * - Includes pagination metadata and links when applicable
   337	 *
   338	 * Data flow:
   339	 * 1. Query operations fetch primary records and optional includes
   340	 * 2. Records are transformed to JSON:API format via toJsonApiRecord
   341	 * 3. buildJsonApiResponse adds relationship objects for all foreign keys
   342	 * 4. Adds proper links (self, related) for navigating the API
   343	 * 5. Assembles included resources with their own links
   344	 * 6. Adds pagination metadata and links if provided
   345	 * 7. Returns complete JSON:API document ready for HTTP response
   346	 */
   347	export const buildJsonApiResponse = async (scope, records, included = [], isSingle = false, scopeName, context) => {
   348	  const {
   349	    vars: {
   350	      schemaInfo: { schemaInstance, schemaRelationships: relationships, idProperty }
   351	    }
   352	  } = scope
   353	
   354	  const idField = idProperty || 'id'
   355	
   356	  const schemaStructure = getSchemaStructure(schemaInstance)
   357	
   358	  const processedRecords = records.map(record => {
   359	    const { [RELATIONSHIPS_KEY]: _relationships, ...cleanRecord } = record
   360	    const jsonApiRecord = toJsonApiRecord(scope, cleanRecord, scopeName)
   361	
   362	    if (_relationships) {
   363	      jsonApiRecord.relationships = _relationships
   364	    }
   365	
   366	    for (const [fieldName, fieldDef] of Object.entries(schemaStructure)) {
   367	      if (fieldDef.belongsTo && fieldDef.as && fieldName in cleanRecord) {
   368	        jsonApiRecord.relationships = jsonApiRecord.relationships || {}
   369	        if (!jsonApiRecord.relationships[fieldDef.as]) {
   370	          if (cleanRecord[fieldName] !== null && cleanRecord[fieldName] !== undefined) {
   371	            const relationshipObject = {
   372	              data: {
   373	                type: fieldDef.belongsTo,
   374	                id: String(cleanRecord[fieldName])
   375	              }
   376	            }
   377	
   378	            relationshipObject.links = {
   379	              self: buildRelationshipUrl(context, scope, scopeName, record[idField], fieldDef.as, true),
   380	              related: buildRelationshipUrl(context, scope, scopeName, record[idField], fieldDef.as, false)
   381	            }
   382	
   383	            jsonApiRecord.relationships[fieldDef.as] = relationshipObject
   384	          } else {
   385	            const relationshipObject = {
   386	              data: null
   387	            }
   388	
   389	            relationshipObject.links = {
   390	              self: buildRelationshipUrl(context, scope, scopeName, record[idField], fieldDef.as, true),
   391	              related: buildRelationshipUrl(context, scope, scopeName, record[idField], fieldDef.as, false)
   392	            }
   393	
   394	            jsonApiRecord.relationships[fieldDef.as] = relationshipObject
   395	          }
   396	        }
   397	      }
   398	    }
   399	
   400	    Object.entries(relationships || {}).forEach(([relName, relDef]) => {
   401	      if (relDef.belongsToPolymorphic) {
   402	        const typeValue = cleanRecord[relDef.belongsToPolymorphic.typeField]
   403	        const idValue = cleanRecord[relDef.belongsToPolymorphic.idField]
   404	
   405	        if (typeValue && idValue) {
   406	          jsonApiRecord.relationships = jsonApiRecord.relationships || {}
   407	          const relationshipObject = {
   408	            data: {
   409	              type: typeValue,
   410	              id: String(idValue)
   411	            }
   412	          }
   413	
   414	          relationshipObject.links = {
   415	            self: buildRelationshipUrl(context, scope, scopeName, record[idField], relName, true),
   416	            related: buildRelationshipUrl(context, scope, scopeName, record[idField], relName, false)
   417	          }
   418	
   419	          jsonApiRecord.relationships[relName] = relationshipObject
   420	        } else if (typeValue === null || idValue === null) {
   421	          jsonApiRecord.relationships = jsonApiRecord.relationships || {}
   422	          const relationshipObject = {
   423	            data: null
   424	          }
   425	
   426	          relationshipObject.links = {
   427	            self: buildRelationshipUrl(context, scope, scopeName, record[idField], relName, true),
   428	            related: buildRelationshipUrl(context, scope, scopeName, record[idField], relName, false)
   429	          }
   430	
   431	          jsonApiRecord.relationships[relName] = relationshipObject
   432	        }
   433	      }
   434	    })
   435	
   436	    return jsonApiRecord
   437	  })
   438	
   439	  const urlPrefix = context.urlPrefix || scope.vars.transport?.mountPath || ''
   440	  const normalizedData = isSingle ? processedRecords[0] : processedRecords
   441	
   442	  if (normalizedData) {
   443	    if (Array.isArray(normalizedData)) {
   444	      normalizedData.forEach(item => {
   445	        if (!item.links) item.links = {}
   446	        item.links.self = buildResourceUrl(context, scope, scopeName, item.id)
   447	      })
   448	    } else {
   449	      if (!normalizedData.links) normalizedData.links = {}
   450	      normalizedData.links.self = buildResourceUrl(context, scope, scopeName, normalizedData.id)
   451	    }
   452	  }
   453	
   454	  const response = {
   455	    data: normalizedData
   456	  }
   457	
   458	  if (included.length > 0) {
   459	    included.forEach(item => {
   460	      if (!item.links) item.links = {}
   461	      item.links.self = buildResourceUrl(context, scope, item.type, item.id)
   462	    })
   463	
   464	    response.included = included
   465	  }
   466	
   467	  if (context?.returnMeta?.paginationMeta) {
   468	    response.meta = {
   469	      pagination: context.returnMeta.paginationMeta
   470	    }
   471	  }
   472	
   473	  if (context?.returnMeta?.paginationLinks) {
   474	    response.links = context.returnMeta.paginationLinks
   475	  } else {
   476	    const urlPrefix = getUrlPrefix(context, scope)
   477	    response.links = {
   478	      self: isSingle
   479	        ? buildResourceUrl(context, scope, scopeName, normalizedData.id)
   480	        : `${urlPrefix}/${scopeName}${context?.returnMeta?.queryString || ''}`
   481	    }
   482	  }
   483	
   484	  return response
   485	}
