     1	/**
     2	 * Transformation functions for simplified API mode
     3	 *
     4	 * @description
     5	 * This module enables "simplified mode" - a developer-friendly alternative
     6	 * to verbose JSON:API format. It allows APIs to:
     7	 * - Accept plain JavaScript objects as input
     8	 * - Return flat response objects instead of nested JSON:API
     9	 * - Automatically convert between formats transparently
    10	 * - Maintain JSON:API compliance internally
    11	 */
    12	
    13	import { RestApiValidationError } from '../../../../lib/rest-api-errors.js'
    14	
    15	/**
    16	 * Transforms simplified plain object into JSON:API format
    17	 *
    18	 * @param {Object} scope - Scope containing input data
    19	 * @param {Object} scope.inputRecord - Plain object or JSON:API document
    20	 * @param {Object} deps - Dependencies with context
    21	 * @param {string} deps.context.scopeName - Resource type name
    22	 * @param {Object} deps.context.schemaStructure - Schema with field definitions
    23	 * @param {Object} deps.context.schemaRelationships - Relationship configurations
    24	 * @returns {Object} JSON:API formatted document
    25	 *
    26	 * @example
    27	 * // Input: Simple object with only attributes
    28	 * const input = {
    29	 *   id: '123',
    30	 *   title: 'My Article',
    31	 *   content: 'Article content here'
    32	 * };
    33	 * const result = transformSimplifiedToJsonApi(
    34	 *   { inputRecord: input },
    35	 *   { context: { scopeName: 'articles', schemaStructure: schema } }
    36	 * );
    37	 *
    38	 * // Output: Proper JSON:API structure
    39	 * // {
    40	 * //   data: {
    41	 * //     type: 'articles',
    42	 * //     id: '123',
    43	 * //     attributes: {
    44	 * //       title: 'My Article',
    45	 * //       content: 'Article content here'
    46	 * //     }
    47	 * //   }
    48	 * // }
    49	 *
    50	 * @example
    51	 * // Input: Object with relationship name (preferred way)
    52	 * const schema = {
    53	 *   title: { type: 'string' },
    54	 *   author_id: {
    55	 *     type: 'number',
    56	 *     belongsTo: 'users',
    57	 *     as: 'author'
    58	 *   }
    59	 * };
    60	 * const input = {
    61	 *   id: '456',
    62	 *   title: 'Another Article',
    63	 *   author: 789                         // Using relationship name
    64	 * };
    65	 *
    66	 * // Output: Relationship becomes JSON:API relationship
    67	 * // {
    68	 * //   data: {
    69	 * //     type: 'articles',
    70	 * //     id: '456',
    71	 * //     attributes: {
    72	 * //       title: 'Another Article'       // author removed from attributes
    73	 * //     },
    74	 * //     relationships: {
    75	 * //       author: {
    76	 * //         data: { type: 'users', id: '789' }
    77	 * //       }
    78	 * //     }
    79	 * //   }
    80	 * // }
    81	 *
    82	 * @example
    83	 * // Input: Object with relationship name (only supported way)
    84	 * const input = {
    85	 *   id: '456',
    86	 *   title: 'Another Article',
    87	 *   author: 789                         // Using relationship name
    88	 * };
    89	 * // Output: Same JSON:API structure as above
    90	 *
    91	 * @example
    92	 * // Input: Many-to-many relationship
    93	 * const relationships = {
    94	 *   tags: {
    95	 *     type: 'manyToMany',
    96	 *     through: 'article_tags',
    97	 *     foreignKey: 'article_id',
    98	 *     otherKey: 'tag_id',
    99	 *     target: 'tags'
   100	 *   }
   101	 * };
   102	 * const input = {
   103	 *   title: 'Tagged Article',
   104	 *   tags: ['10', '20', '30']            // Array of IDs
   105	 * };
   106	 *
   107	 * // Output: Array becomes relationship data array
   108	 * // {
   109	 * //   data: {
   110	 * //     type: 'articles',
   111	 * //     attributes: {
   112	 * //       title: 'Tagged Article'
   113	 * //     },
   114	 * //     relationships: {
   115	 * //       tags: {
   116	 * //         data: [
   117	 * //           { type: 'tags', id: '10' },
   118	 * //           { type: 'tags', id: '20' },
   119	 * //           { type: 'tags', id: '30' }
   120	 * //         ]
   121	 * //       }
   122	 * //     }
   123	 * //   }
   124	 * // }
   125	 *
   126	 * @example
   127	 * // Input: Already JSON:API formatted
   128	 * const input = {
   129	 *   data: {
   130	 *     type: 'articles',
   131	 *     id: '1',
   132	 *     attributes: { title: 'Already formatted' }
   133	 *   }
   134	 * };
   135	 * // Output: Returned unchanged (detected by data.type)
   136	 *
   137	 * @example
   138	 * // Input: Polymorphic relationship with minimal object format
   139	 * const relationships = {
   140	 *   commentable: {
   141	 *     belongsToPolymorphic: {
   142	 *       types: ['posts', 'videos'],
   143	 *       typeField: 'commentable_type',
   144	 *       idField: 'commentable_id'
   145	 *     }
   146	 *   }
   147	 * };
   148	 * const input = {
   149	 *   text: 'Great content!',
   150	 *   commentable: { id: '456', _type: 'posts' }  // Minimal object format
   151	 * };
   152	 *
   153	 * // Output: Proper JSON:API structure for polymorphic
   154	 * // {
   155	 * //   data: {
   156	 * //     type: 'comments',
   157	 * //     attributes: {
   158	 * //       text: 'Great content!'
   159	 * //     },
   160	 * //     relationships: {
   161	 * //       commentable: {
   162	 * //         data: { type: 'posts', id: '456' }
   163	 * //       }
   164	 * //     }
   165	 * //   }
   166	 * // }
   167	 *
   168	 * @example
   169	 * // Input: Clearing polymorphic relationship
   170	 * const input = {
   171	 *   text: 'Updated comment',
   172	 *   commentable: null  // Clear the relationship
   173	 * };
   174	 *
   175	 * // Output: Relationship set to null
   176	 * // {
   177	 * //   data: {
   178	 * //     type: 'comments',
   179	 * //     attributes: {
   180	 * //       text: 'Updated comment'
   181	 * //     },
   182	 * //     relationships: {
   183	 * //       commentable: { data: null }
   184	 * //     }
   185	 * //   }
   186	 * // }
   187	 *
   188	 * @description
   189	 * Used by:
   190	 * - rest-api-plugin for POST/PUT/PATCH when simplified: true
   191	 * - Processes input before validation and storage
   192	 *
   193	 * Purpose:
   194	 * - Provides developer-friendly plain object API
   195	 * - Automatically detects and converts relationships
   196	 * - Supports relationship alias notation
   197	 * - Supports polymorphic relationships with minimal object format
   198	 * - Maintains JSON:API compliance internally
   199	 * - Allows gradual migration from simple to JSON:API
   200	 *
   201	 * Data flow:
   202	 * 1. Checks if already JSON:API (has data.type)
   203	 * 2. Extracts ID from input object
   204	 * 3. Processes belongsTo relationships from schema
   205	 * 4. Processes hasMany/manyToMany from relationships
   206	 * 5. Processes polymorphic belongsTo from relationships
   207	 * 6. Remaining fields become attributes
   208	 * 6. Builds proper JSON:API document structure
   209	 */
   210	export const transformSimplifiedToJsonApi = (scope, deps) => {
   211	  // Extract needed values from scope and deps
   212	  const input = scope.inputRecord
   213	  const scopeName = deps.context.scopeName
   214	  const schema = deps.context.schemaStructure
   215	  const relationships = deps.context.schemaRelationships
   216	
   217	  // If already JSON:API format, return as-is
   218	  if (input?.data?.type) {
   219	    return input
   220	  }
   221	
   222	  const attributes = {}
   223	  const relationshipsData = {}
   224	  const tempInput = { ...input } // Create a mutable copy of the input
   225	
   226	  const id = tempInput.id
   227	  delete tempInput.id // Extract ID from tempInput
   228	
   229	  // 1. Process belongsTo relationships (from schema fields)
   230	  // Iterate through the schema structure to find belongsTo fields
   231	  for (const [fieldName, fieldDef] of Object.entries(schema)) {
   232	    if (fieldDef.belongsTo) {
   233	      const relAlias = fieldDef.as || fieldName // Use 'as' alias or foreign key name as default alias
   234	      let valueToProcess
   235	
   236	      // Check if the user provided the foreign key name (e.g., country_id) - NO LONGER ALLOWED
   237	      if (tempInput[fieldName] !== undefined) {
   238	        throw new RestApiValidationError(
   239	          `Foreign key field '${fieldName}' is no longer supported for relationship input. ` +
   240	          `Use the relationship name '${fieldDef.as || fieldName}' instead.`,
   241	          {
   242	            fields: [fieldName],
   243	            violations: [{
   244	              field: fieldName,
   245	              rule: 'deprecated_foreign_key',
   246	              message: 'Use relationship name instead of foreign key field'
   247	            }]
   248	          }
   249	        )
   250	      }
   251	
   252	      // Check if the user provided the relationship name (e.g., country)
   253	      const relName = fieldDef.as || fieldName
   254	      if (tempInput[relName] !== undefined) {
   255	        valueToProcess = tempInput[relName]
   256	        delete tempInput[relName] // Remove from tempInput once processed
   257	      }
   258	
   259	      if (valueToProcess !== undefined) {
   260	        relationshipsData[relAlias] = {
   261	          data: valueToProcess ? { type: fieldDef.belongsTo, id: String(valueToProcess) } : null
   262	        }
   263	      }
   264	    }
   265	  }
   266	
   267	  // 2. Process hasMany/manyToMany relationships (from relationships object)
   268	  // Iterate through the relationships config to find hasMany/manyToMany
   269	  for (const [relName, relConfig] of Object.entries(relationships || {})) {
   270	    // Check if the user provided data for this relationship name
   271	    if (tempInput[relName] !== undefined) {
   272	      const value = tempInput[relName]
   273	      delete tempInput[relName] // Remove from tempInput once processed
   274	
   275	      if ((relConfig.type === 'hasMany' || relConfig.type === 'manyToMany') && Array.isArray(value)) {
   276	        // Determine the target type based on relationship type:
   277	        // - For manyToMany: the targetType is the relationship name itself
   278	        // - For hasMany/hasOne: the targetType is specified in the target property
   279	        const targetType = relConfig.type === 'manyToMany' ? relName : relConfig.target
   280	        relationshipsData[relName] = {
   281	          data: value.map(relId => ({ type: targetType, id: String(relId) }))
   282	        }
   283	      }
   284	      // Handle hasOne relationships
   285	      else if (relConfig.type === 'hasOne' && value !== undefined) {
   286	        relationshipsData[relName] = {
   287	          data: value ? { type: relConfig.target, id: String(value) } : null
   288	        }
   289	      }
   290	      // Handle single belongsTo relationships if they were also defined in 'relationships'
   291	      // (though typically they are defined in schema.belongsTo)
   292	      else if (relConfig.belongsTo && value !== undefined) {
   293	        relationshipsData[relName] = {
   294	          data: value ? { type: relConfig.belongsTo, id: String(value) } : null
   295	        }
   296	      }
   297	      // Handle polymorphic belongsTo relationships with minimal object format
   298	      else if (relConfig.belongsToPolymorphic && value !== undefined) {
   299	        if (value === null) {
   300	          relationshipsData[relName] = { data: null }
   301	        } else if (typeof value === 'object' && value !== null && value.id && value._type) {
   302	          relationshipsData[relName] = {
   303	            data: { type: value._type, id: String(value.id) }
   304	          }
   305	        } else {
   306	          throw new RestApiValidationError(
   307	            `Invalid format for polymorphic relationship '${relName}'. Expected { id: 'value', _type: 'type' } or null`,
   308	            {
   309	              fields: [relName],
   310	              violations: [{
   311	                field: relName,
   312	                rule: 'polymorphic_format',
   313	                message: 'Must be an object with id and _type properties, or null'
   314	              }]
   315	            }
   316	          )
   317	        }
   318	      }
   319	    }
   320	  }
   321	
   322	  // 3. Any remaining fields in tempInput are attributes
   323	  Object.assign(attributes, tempInput)
   324	
   325	  return {
   326	    data: {
   327	      type: scopeName,
   328	      ...(id && { id: String(id) }),
   329	      ...(Object.keys(attributes).length > 0 && { attributes }),
   330	      ...(Object.keys(relationshipsData).length > 0 && { relationships: relationshipsData })
   331	    }
   332	  }
   333	}
   334	
   335	/**
   336	 * Transforms JSON:API response into simplified plain object
   337	 *
   338	 * @param {Object} scope - Scope containing JSON:API response
   339	 * @param {Object} scope.record - JSON:API document to transform
   340	 * @param {Object} deps - Dependencies with context
   341	 * @param {Object} deps.context.schemaStructure - Schema for mapping relationships
   342	 * @param {Object} deps.context.schemaRelationships - Relationship configurations
   343	 * @param {Object} deps.context.scopes - All scopes for nested transformations
   344	 * @returns {Object|Object[]} Simplified object or array of objects
   345	 *
   346	 * @example
   347	 * // Input: Single resource JSON:API
   348	 * const jsonApi = {
   349	 *   data: {
   350	 *     type: 'articles',
   351	 *     id: '123',
   352	 *     attributes: {
   353	 *       title: 'My Article',
   354	 *       content: 'Content here'
   355	 *     }
   356	 *   }
   357	 * };
   358	 * const result = transformJsonApiToSimplified(
   359	 *   { record: jsonApi },
   360	 *   { context: { schemaStructure: schema } }
   361	 * );
   362	 *
   363	 * // Output: Flattened object
   364	 * // {
   365	 * //   id: '123',
   366	 * //   title: 'My Article',        // Attributes flattened
   367	 * //   content: 'Content here'
   368	 * // }
   369	 *
   370	 * @example
   371	 * // Input: Resource with belongsTo relationship
   372	 * const schema = {
   373	 *   title: { type: 'string' },
   374	 *   author_id: { type: 'number', belongsTo: 'users', as: 'author' }
   375	 * };
   376	 * const jsonApi = {
   377	 *   data: {
   378	 *     type: 'articles',
   379	 *     id: '456',
   380	 *     attributes: { title: 'Article with Author' },
   381	 *     relationships: {
   382	 *       author: {
   383	 *         data: { type: 'users', id: '789' }
   384	 *       }
   385	 *     }
   386	 *   }
   387	 * };
   388	 *
   389	 * // Output: Relationship becomes foreign key
   390	 * // {
   391	 * //   id: '456',
   392	 * //   title: 'Article with Author',
   393	 * //   author_id: '789'              // Restored from relationship
   394	 * // }
   395	 *
   396	 * @example
   397	 * // Input: Collection response
   398	 * const jsonApi = {
   399	 *   data: [
   400	 *     {
   401	 *       type: 'articles',
   402	 *       id: '1',
   403	 *       attributes: { title: 'First Article' }
   404	 *     },
   405	 *     {
   406	 *       type: 'articles',
   407	 *       id: '2',
   408	 *       attributes: { title: 'Second Article' }
   409	 *     }
   410	 *   ],
   411	 *   meta: { total: 2 },
   412	 *   links: { next: '/articles?page=2' }
   413	 * };
   414	 *
   415	 * // Output: Preserves meta and links
   416	 * // {
   417	 * //   data: [
   418	 * //     { id: '1', title: 'First Article' },
   419	 * //     { id: '2', title: 'Second Article' }
   420	 * //   ],
   421	 * //   meta: { total: 2 },
   422	 * //   links: { next: '/articles?page=2' }
   423	 * // }
   424	 *
   425	 * @example
   426	 * // Input: With included resources
   427	 * const jsonApi = {
   428	 *   data: {
   429	 *     type: 'articles',
   430	 *     id: '400',
   431	 *     attributes: { title: 'JavaScript Guide' },
   432	 *     relationships: {
   433	 *       author: {
   434	 *         data: { type: 'authors', id: '75' }
   435	 *       },
   436	 *       chapters: {
   437	 *         data: [
   438	 *           { type: 'chapters', id: '1001' },
   439	 *           { type: 'chapters', id: '1002' }
   440	 *         ]
   441	 *       }
   442	 *     }
   443	 *   },
   444	 *   included: [
   445	 *     {
   446	 *       type: 'authors',
   447	 *       id: '75',
   448	 *       attributes: { name: 'Jane Smith' }
   449	 *     },
   450	 *     {
   451	 *       type: 'chapters',
   452	 *       id: '1001',
   453	 *       attributes: { number: 1, title: 'Introduction' }
   454	 *     },
   455	 *     {
   456	 *       type: 'chapters',
   457	 *       id: '1002',
   458	 *       attributes: { number: 2, title: 'Basics' }
   459	 *     }
   460	 *   ]
   461	 * };
   462	 *
   463	 * // Output: Expands included resources
   464	 * // {
   465	 * //   id: '400',
   466	 * //   title: 'JavaScript Guide',
   467	 * //   author_id: '75',
   468	 * //   author: {                    // Expanded from included
   469	 * //     id: '75',
   470	 * //     name: 'Jane Smith'
   471	 * //   },
   472	 * //   chapters: [{ id: '1001' }, { id: '1002' }],
   473	 * //   chapters: [                  // Expanded array
   474	 * //     { id: '1001', number: 1, title: 'Introduction' },
   475	 * //     { id: '1002', number: 2, title: 'Basics' }
   476	 * //   ]
   477	 * // }
   478	 *
   479	 * @description
   480	 * Used by:
   481	 * - rest-api-plugin for GET/QUERY when simplified: true
   482	 * - Transforms responses before sending to client
   483	 *
   484	 * Purpose:
   485	 * - Provides cleaner response format for clients
   486	 * - Creates minimal relationship objects from relationships
   487	 * - Expands included resources for convenience
   488	 * - Maintains compatibility with collection metadata
   489	 * - Handles both single and array responses
   490	 *
   491	 * Data flow:
   492	 * 1. Detects array vs single resource
   493	 * 2. For arrays, preserves meta/links at root
   494	 * 3. Calls transformSingleJsonApiToSimplified for each
   495	 * 4. Returns simplified structure
   496	 */
   497	export const transformJsonApiToSimplified = (scope, deps) => {
   498	  // Extract needed values from scope and deps
   499	  const jsonApi = scope.record
   500	  const schema = deps.context.schemaStructure
   501	  const relationships = deps.context.schemaRelationships
   502	  const scopes = deps.context.scopes
   503	
   504	  if (!jsonApi?.data) return jsonApi
   505	
   506	  // Handle array response (QUERY)
   507	  if (Array.isArray(jsonApi.data)) {
   508	    const simplifiedData = jsonApi.data.map(item =>
   509	      transformSingleJsonApiToSimplified(
   510	        { data: item, included: jsonApi.included },
   511	        { context: { schemaStructure: schema, schemaRelationships: relationships, scopes } }
   512	      )
   513	    )
   514	
   515	    // For query results, return object with data, meta, and links
   516	    const result = { data: simplifiedData }
   517	
   518	    // Add meta if present
   519	    if (jsonApi.meta) {
   520	      result.meta = jsonApi.meta
   521	    }
   522	
   523	    // Add links if present
   524	    if (jsonApi.links) {
   525	      result.links = jsonApi.links
   526	    }
   527	
   528	    return result
   529	  }
   530	
   531	  // Handle single response (no change for single resources)
   532	  return transformSingleJsonApiToSimplified(
   533	    { data: jsonApi.data, included: jsonApi.included },
   534	    { context: { schemaStructure: schema, schemaRelationships: relationships, scopes } }
   535	  )
   536	}
   537	
   538	/**
   539	 * Transforms single JSON:API resource into simplified format
   540	 *
   541	 * @param {Object} scope - Scope with data and included resources
   542	 * @param {Object} scope.data - Single JSON:API resource object
   543	 * @param {Array} scope.included - Array of included resources
   544	 * @param {Object} deps - Dependencies with context
   545	 * @param {Object} deps.context.schemaStructure - Resource schema
   546	 * @param {Object} deps.context.schemaRelationships - Relationship configs
   547	 * @param {Object} deps.context.scopes - All scopes for recursion
   548	 * @returns {Object} Simplified plain object
   549	 *
   550	 * @example
   551	 * // Input: Basic resource with relationships
   552	 * const data = {
   553	 *   type: 'comments',
   554	 *   id: '100',
   555	 *   attributes: {
   556	 *     text: 'Great article!',
   557	 *     created_at: '2024-01-15'
   558	 *   },
   559	 *   relationships: {
   560	 *     author: {
   561	 *       data: { type: 'users', id: '50' }
   562	 *     },
   563	 *     article: {
   564	 *       data: { type: 'articles', id: '200' }
   565	 *     }
   566	 *   }
   567	 * };
   568	 * const schema = {
   569	 *   text: { type: 'string' },
   570	 *   created_at: { type: 'datetime' },
   571	 *   author_id: { type: 'number', belongsTo: 'users', as: 'author' },
   572	 *   article_id: { type: 'number', belongsTo: 'articles', as: 'article' }
   573	 * };
   574	 * const result = transformSingleJsonApiToSimplified(
   575	 *   { data: data, included: null },
   576	 *   { context: { schemaStructure: schema } }
   577	 * );
   578	 *
   579	 * // Output: Minimal relationship objects
   580	 * // {
   581	 * //   id: '100',
   582	 * //   text: 'Great article!',
   583	 * //   created_at: '2024-01-15',
   584	 * //   author: { id: '50' },          // Minimal object from relationships.author
   585	 * //   article: { id: '200' }         // Minimal object from relationships.article
   586	 * // }
   587	 *
   588	 * @example
   589	 * // Input: Many-to-many relationships
   590	 * const data = {
   591	 *   type: 'articles',
   592	 *   id: '300',
   593	 *   attributes: { title: 'My Article' },
   594	 *   relationships: {
   595	 *     tags: {
   596	 *       data: [
   597	 *         { type: 'tags', id: '1' },
   598	 *         { type: 'tags', id: '2' },
   599	 *         { type: 'tags', id: '3' }
   600	 *       ]
   601	 *     }
   602	 *   }
   603	 * };
   604	 * const relationships = {
   605	 *   tags: { manyToMany: { via: 'article_tags' } }
   606	 * };
   607	 *
   608	 * // Output: Minimal relationship objects
   609	 * // {
   610	 * //   id: '300',
   611	 * //   title: 'My Article',
   612	 * //   tags: [{ id: '1' }, { id: '2' }, { id: '3' }]   // Minimal objects
   613	 * // }
   614	 *
   615	 * @example
   616	 * // Input: Polymorphic relationship
   617	 * const data = {
   618	 *   type: 'reviews',
   619	 *   id: '500',
   620	 *   attributes: { rating: 5, comment: 'Great!' },
   621	 *   relationships: {
   622	 *     reviewable: {
   623	 *       data: { type: 'authors', id: '75' }
   624	 *     }
   625	 *   }
   626	 * };
   627	 * const relationships = {
   628	 *   reviewable: {
   629	 *     belongsToPolymorphic: {
   630	 *       types: ['authors', 'publishers'],
   631	 *       typeField: 'reviewable_type',
   632	 *       idField: 'reviewable_id'
   633	 *     }
   634	 *   }
   635	 * };
   636	 *
   637	 * // Output: Minimal polymorphic object with type
   638	 * // {
   639	 * //   id: '500',
   640	 * //   rating: 5,
   641	 * //   comment: 'Great!',
   642	 * //   reviewable: { id: '75', type: 'authors' }   // Minimal object with type
   643	 * // }
   644	 *
   645	 * @example
   646	 * // Input: Polymorphic relationship with included resource
   647	 * const dataWithInclude = {
   648	 *   type: 'reviews',
   649	 *   id: '500',
   650	 *   attributes: { rating: 5, comment: 'Great!' },
   651	 *   relationships: {
   652	 *     reviewable: {
   653	 *       data: { type: 'authors', id: '75' }
   654	 *     }
   655	 *   }
   656	 * };
   657	 * const included = [
   658	 *   {
   659	 *     type: 'authors',
   660	 *     id: '75',
   661	 *     attributes: { name: 'Jane Smith', bio: 'Fantastic author' }
   662	 *   }
   663	 * ];
   664	 *
   665	 * // Output: Full polymorphic object (type field removed as redundant)
   666	 * // {
   667	 * //   id: '500',
   668	 * //   rating: 5,
   669	 * //   comment: 'Great!',
   670	 * //   reviewable: {                // Full object when included
   671	 * //     id: '75',
   672	 * //     name: 'Jane Smith',
   673	 * //     bio: 'Fantastic author'
   674	 * //   }
   675	 * // }
   676	 *
   677	 * @example
   678	 * // Input: With included resources (recursive expansion)
   679	 * const data = {
   680	 *   type: 'books',
   681	 *   id: '400',
   682	 *   attributes: { title: 'JavaScript Guide' },
   683	 *   relationships: {
   684	 *     author: {
   685	 *       data: { type: 'authors', id: '75' }
   686	 *     },
   687	 *     chapters: {
   688	 *       data: [
   689	 *         { type: 'chapters', id: '1001' },
   690	 *         { type: 'chapters', id: '1002' }
   691	 *       ]
   692	 *     }
   693	 *   }
   694	 * };
   695	 * const included = [
   696	 *   {
   697	 *     type: 'authors',
   698	 *     id: '75',
   699	 *     attributes: { name: 'Jane Smith' },
   700	 *     relationships: {
   701	 *       company: {
   702	 *         data: { type: 'companies', id: '5' }
   703	 *       }
   704	 *     }
   705	 *   },
   706	 *   {
   707	 *     type: 'chapters',
   708	 *     id: '1001',
   709	 *     attributes: { number: 1, title: 'Introduction' }
   710	 *   },
   711	 *   {
   712	 *     type: 'chapters',
   713	 *     id: '1002',
   714	 *     attributes: { number: 2, title: 'Basics' }
   715	 *   }
   716	 * ];
   717	 *
   718	 * // Output: Recursively expanded
   719	 * // {
   720	 * //   id: '400',
   721	 * //   title: 'JavaScript Guide',
   722	 * //   author: {                    // Expanded and transformed
   723	 * //     id: '75',
   724	 * //     name: 'Jane Smith',
   725	 * //     company: { id: '5' }       // Author's relationships processed as minimal objects!
   726	 * //   },
   727	 * //   chapters: [
   728	 * //     { id: '1001', number: 1, title: 'Introduction' },
   729	 * //     { id: '1002', number: 2, title: 'Basics' }
   730	 * //   ]
   731	 * // }
   732	 *
   733	 * @description
   734	 * Used by:
   735	 * - transformJsonApiToSimplified for each resource
   736	 * - Calls itself recursively for included resources
   737	 *
   738	 * Purpose:
   739	 * - Core transformation logic for single resources
   740	 * - Creates minimal relationship objects from relationships
   741	 * - Creates arrays of minimal relationship objects for to-many relationships
   742	 * - Recursively expands and transforms included data
   743	 * - Handles polymorphic relationships
   744	 *
   745	 * Data flow:
   746	 * 1. Copies ID and attributes to result
   747	 * 2. Processes each relationship:
   748	 *    - BelongsTo: creates minimal relationship object
   749	 *    - Polymorphic: creates minimal relationship object with type
   750	 *    - HasMany/ManyToMany: creates array of minimal relationship objects
   751	 * 3. If included data exists:
   752	 *    - Finds matching included resources
   753	 *    - Recursively transforms them (preserving their relationships!)
   754	 *    - Adds transformed objects/arrays to result
   755	 *
   756	 * @private
   757	 */
   758	export const transformSingleJsonApiToSimplified = (scope, deps) => {
   759	  // Extract needed values from scope and deps
   760	  const data = scope.data
   761	  const included = scope.included
   762	  const schema = deps.context.schemaStructure
   763	  const relationships = deps.context.schemaRelationships
   764	  const scopes = deps.context.scopes
   765	
   766	  const simplified = {}
   767	
   768	  // Add ID
   769	  if (data.id) {
   770	    simplified.id = data.id
   771	  }
   772	
   773	  // Add attributes
   774	  Object.assign(simplified, data.attributes || {})
   775	
   776	  // Process relationships to create minimal objects
   777	  if (data.relationships) {
   778	    for (const [relName, relData] of Object.entries(data.relationships)) {
   779	      // Find schema field for this belongsTo relationship
   780	      const schemaEntry = Object.entries(schema).find(([_, def]) => def.as === relName)
   781	
   782	      if (schemaEntry) {
   783	        const [fieldName, fieldDef] = schemaEntry
   784	        if (fieldDef.belongsTo && relData.data) {
   785	          // Create minimal relationship object (no more foreign key field)
   786	          simplified[relName] = { id: relData.data.id }
   787	        }
   788	      }
   789	
   790	      // Check if this is a polymorphic relationship from the relationships config
   791	      const rel = relationships?.[relName]
   792	      if (rel?.belongsToPolymorphic && relData.data) {
   793	        // Create minimal relationship object for polymorphic (no more type/id fields)
   794	        simplified[relName] = { id: relData.data.id, _type: relData.data.type }
   795	      }
   796	
   797	      // Handle to-many relationships (create minimal objects with just IDs)
   798	      if (rel?.type === 'hasMany' || rel?.type === 'manyToMany') {
   799	        if (relData.data && Array.isArray(relData.data)) {
   800	          simplified[relName] = relData.data.map(item => ({ id: item.id }))
   801	        }
   802	      }
   803	
   804	      // Handle includes (nested objects)
   805	      if (included && relData.data) {
   806	        const findIncluded = (ref) =>
   807	          included.find(inc => inc.type === ref.type && inc.id === ref.id)
   808	
   809	        if (Array.isArray(relData.data)) {
   810	          const nestedData = relData.data.map(findIncluded).filter(Boolean)
   811	          if (nestedData.length > 0) {
   812	            simplified[relName] = nestedData.map(item => {
   813	              // For each included record, transform it recursively to restore its relationship fields
   814	              const itemSimplified = transformSingleJsonApiToSimplified(
   815	                { data: item, included: null },
   816	                {
   817	                  context: {
   818	                    schemaStructure: scopes?.[item.type]?.vars?.schemaInfo?.schema || {},
   819	                    schemaRelationships: scopes?.[item.type]?.vars?.schemaInfo?.schemaRelationships || {},
   820	                    scopes
   821	                  }
   822	                }
   823	              )
   824	              // For polymorphic relationships, add _type field to identify the resource type
   825	              if (rel?.belongsToPolymorphic) {
   826	                itemSimplified._type = item.type
   827	              }
   828	              return itemSimplified
   829	            })
   830	          }
   831	        } else {
   832	          const nestedData = findIncluded(relData.data)
   833	          if (nestedData) {
   834	            // Transform the single included record to restore its relationship fields
   835	            const itemSimplified = transformSingleJsonApiToSimplified(
   836	              { data: nestedData, included: null },
   837	              {
   838	                context: {
   839	                  schemaStructure: scopes?.[nestedData.type]?.vars?.schemaInfo?.schema || {},
   840	                  schemaRelationships: scopes?.[nestedData.type]?.vars?.schemaInfo?.schemaRelationships || {},
   841	                  scopes
   842	                }
   843	              }
   844	            )
   845	            // For polymorphic relationships, add _type field to identify the resource type
   846	            if (rel?.belongsToPolymorphic) {
   847	              itemSimplified._type = nestedData.type
   848	            }
   849	            simplified[relName] = itemSimplified
   850	          }
   851	        }
   852	      }
   853	    }
   854	  }
   855	
   856	  return simplified
   857	}
