     1	/**
     2	 * Checks if a field exists only in code, not in the database
     3	 *
     4	 * @param {string} fieldName - The field name to check
     5	 * @param {Object} schemaInfo - Schema info with computed fields and structure
     6	 * @returns {boolean} True if field is computed or virtual
     7	 *
     8	 * @example
     9	 * // Input: Check computed field
    10	 * const schemaInfo = {
    11	 *   computed: { profit_margin: { (definition) } },
    12	 *   schemaStructure: { price: { type: 'number' } }
    13	 * };
    14	 * isNonDatabaseField('profit_margin', schemaInfo);
    15	 * // Output: true (it's computed, not stored)
    16	 *
    17	 * @example
    18	 * // Input: Check virtual field
    19	 * const schemaInfo = {
    20	 *   computed: {},
    21	 *   schemaStructure: {
    22	 *     temp_password: { type: 'string', virtual: true }
    23	 *   }
    24	 * };
    25	 * isNonDatabaseField('temp_password', schemaInfo);
    26	 * // Output: true (it's virtual, not stored)
    27	 *
    28	 * @description
    29	 * Used by:
    30	 * - buildFieldSelection to exclude from SELECT queries
    31	 * - dataPost/dataPatch to skip non-database fields
    32	 * - enrichAttributes to identify computed fields
    33	 *
    34	 * Purpose:
    35	 * - Prevents SQL errors by excluding non-existent columns
    36	 * - Identifies fields that need computation or special handling
    37	 * - Supports virtual fields for temporary/input-only data
    38	 */
    39	export const isNonDatabaseField = (fieldName, schemaInfo) => {
    40	  const { computed = {}, schemaStructure = {} } = schemaInfo
    41	  if (fieldName in computed) return true
    42	  const fieldDef = schemaStructure[fieldName]
    43	  return fieldDef && fieldDef.virtual === true
    44	}
    45	
    46	/**
    47	 * Builds the field selection list for database queries with sparse fieldset support
    48	 *
    49	 * @param {Object} scope - Resource scope with schema information
    50	 * @param {Object} deps - Dependencies object
    51	 * @param {Object} deps.context - Request context with queryParams and schemaInfo
    52	 * @returns {Promise<Object>} Field selection information
    53	 *
    54	 * @example
    55	 * // Input: Basic sparse fieldset request
    56	 * const deps = {
    57	 *   context: {
    58	 *     scopeName: 'articles',
    59	 *     queryParams: { fields: { articles: 'title,body' } },
    60	 *     schemaInfo: { idProperty: 'id', ...  }
    61	 *   }
    62	 * };
    63	 * const result = await buildFieldSelection(scope, deps);
    64	 *
    65	 * // Output: Includes requested fields plus required fields
    66	 * // {
    67	 * //   fieldsToSelect: ['id', 'title', 'body', 'author_id', 'category_id'],
    68	 * //   requestedFields: ['title', 'body'],
    69	 * //   computedDependencies: [],
    70	 * //   idProperty: 'id'
    71	 * // }
    72	 * // Note: Foreign keys (author_id, category_id) always included for relationships
    73	 *
    74	 * @example
    75	 * // Input: Computed field with hidden dependencies
    76	 * const schemaInfo = {
    77	 *   computed: {
    78	 *     profit_margin: {
    79	 *       compute: (record) => (record.price - record.cost) / record.price,
    80	 *       dependencies: ['price', 'cost']
    81	 *     }
    82	 *   },
    83	 *   schemaStructure: {
    84	 *     name: { type: 'string' },
    85	 *     price: { type: 'number' },
    86	 *     cost: { type: 'number', normallyHidden: true }
    87	 *   }
    88	 * };
    89	 * const deps = {
    90	 *   context: {
    91	 *     queryParams: { fields: { products: 'name,profit_margin' } }
    92	 *   }
    93	 * };
    94	 *
    95	 * // Output: Fetches hidden dependency for computation
    96	 * // {
    97	 * //   fieldsToSelect: ['id', 'name', 'price', 'cost'],
    98	 * //   requestedFields: ['name', 'profit_margin'],
    99	 * //   computedDependencies: ['cost'], // Will be removed after computation
   100	 * //   idProperty: 'id'
   101	 * // }
   102	 *
   103	 * @example
   104	 * // Input: No sparse fieldsets (returns all visible fields)
   105	 * const deps = {
   106	 *   context: {
   107	 *     queryParams: {}, // No fields parameter
   108	 *     schemaInfo: { ... }
   109	 *   }
   110	 * };
   111	 *
   112	 * // Output: All non-hidden, non-virtual fields
   113	 * // {
   114	 * //   fieldsToSelect: ['id', 'title', 'body', 'published_at', 'author_id'],
   115	 * //   requestedFields: null,
   116	 * //   computedDependencies: [],
   117	 * //   idProperty: 'id'
   118	 * // }
   119	 *
   120	 * @description
   121	 * Used by:
   122	 * - dataGet to build SELECT query
   123	 * - dataQuery to build SELECT query
   124	 * - buildQuerySelection as the main field resolver
   125	 *
   126	 * Purpose:
   127	 * - Implements JSON:API sparse fieldsets specification
   128	 * - Ensures relationships work by including foreign keys
   129	 * - Handles computed field dependencies intelligently
   130	 * - Respects field visibility rules (hidden/normallyHidden)
   131	 * - Supports custom ID column names
   132	 *
   133	 * Data flow:
   134	 * 1. Always includes ID (with aliasing if needed)
   135	 * 2. Parses requested fields from query string
   136	 * 3. Validates requested fields exist in schema
   137	 * 4. Adds dependencies for computed fields
   138	 * 5. Includes all foreign keys for relationships
   139	 * 6. Returns complete field list for SQL SELECT
   140	 */
   141	export const buildFieldSelection = async (scope, deps) => {
   142	  const fieldsToSelect = new Set()
   143	  const computedDependencies = new Set()
   144	
   145	  // Extract values from scope
   146	  const {
   147	    vars: {
   148	      schemaInfo: { schemaInstance, computed: computedFields = {}, schemaStructure }
   149	    }
   150	  } = scope
   151	
   152	  // Extract values from deps
   153	  const { context } = deps
   154	  const scopeName = context.scopeName
   155	  const requestedFields = context.queryParams?.fields?.[scopeName]
   156	  const idProperty = context.schemaInfo.idProperty
   157	
   158	  // Always include the ID field - required for JSON:API
   159	  // Handle aliasing if idProperty is not 'id'
   160	  if (idProperty !== 'id') {
   161	    fieldsToSelect.add(`${idProperty} as id`)
   162	  } else {
   163	    fieldsToSelect.add('id')
   164	  }
   165	
   166	  // Handle both Schema objects and plain objects
   167	  if (!schemaStructure) {
   168	    schemaStructure = schemaInstance?.structure || schemaInstance || {}
   169	  }
   170	
   171	  // Get computed fields and virtual fields from schema
   172	  const computedFieldNames = new Set(Object.keys(computedFields))
   173	
   174	  // Find fields marked as virtual in the schema
   175	  const virtualFieldNames = new Set()
   176	  Object.entries(schemaStructure).forEach(([fieldName, fieldDef]) => {
   177	    if (fieldDef.virtual === true) {
   178	      virtualFieldNames.add(fieldName)
   179	    }
   180	  })
   181	
   182	  const nonDatabaseFields = new Set([...computedFieldNames, ...virtualFieldNames])
   183	
   184	  // Parse requested fields
   185	  const requested = requestedFields
   186	    ? (
   187	        typeof requestedFields === 'string'
   188	          ? requestedFields.split(',').map(f => f.trim()).filter(f => f)
   189	          : requestedFields
   190	      )
   191	    : null
   192	
   193	  if (requested && requested.length > 0) {
   194	    // Sparse fieldsets requested - only select specified fields
   195	    // Example: ?fields[products]=name,price,profit_margin
   196	    requested.forEach(field => {
   197	      // Skip computed and virtual fields - they don't exist in database
   198	      // Computed fields will be calculated later in enrichAttributes
   199	      // Virtual fields are handled separately (from request input)
   200	      if (nonDatabaseFields.has(field)) return
   201	
   202	      const fieldDef = schemaStructure[field]
   203	      if (!fieldDef) throw new Error(`Unknown sparse field '${field}' requested for '${scopeName}'`)
   204	
   205	      if (fieldDef.belongsToPolymorphic) {
   206	        return
   207	      }
   208	
   209	      // NEVER include hidden fields, even if explicitly requested
   210	      // Example: password_hash with hidden:true is never returned
   211	      if (fieldDef.hidden === true) return
   212	
   213	      fieldsToSelect.add(field)
   214	    })
   215	
   216	    // Handle computed field dependencies - fetch fields needed for calculations
   217	    // Example: User requests 'profit_margin' which depends on 'price' and 'cost'
   218	    // We need to fetch price and cost from DB even if not explicitly requested
   219	    const requestedComputedFields = requested.filter(f => computedFieldNames.has(f))
   220	    for (const computedField of requestedComputedFields) {
   221	      const fieldDef = computedFields[computedField]
   222	      if (fieldDef.dependencies) {
   223	        for (const dep of fieldDef.dependencies) {
   224	          const depFieldDef = schemaStructure[dep]
   225	          // Only add dependency if it exists and isn't hidden
   226	          if (depFieldDef && depFieldDef.hidden !== true) {
   227	            fieldsToSelect.add(dep)
   228	            // Track dependencies that weren't explicitly requested
   229	            // These will be removed from the final response
   230	            if (!requested.includes(dep)) {
   231	              computedDependencies.add(dep)
   232	            }
   233	          }
   234	        }
   235	      }
   236	    }
   237	
   238	    // Still handle normallyHidden fields for backward compatibility
   239	    if (requestedComputedFields.length > 0) {
   240	      Object.entries(schemaStructure).forEach(([field, fieldDef]) => {
   241	        if (fieldDef.normallyHidden === true && fieldDef.hidden !== true) {
   242	          // Only add if not already handled by dependencies
   243	          if (!fieldsToSelect.has(field)) {
   244	            fieldsToSelect.add(field)
   245	            if (!requested.includes(field)) {
   246	              computedDependencies.add(field)
   247	            }
   248	          }
   249	        }
   250	      })
   251	    }
   252	  } else {
   253	    // No sparse fieldsets - return all visible fields
   254	    // This is the default behavior when no ?fields parameter is provided
   255	    Object.entries(schemaStructure).forEach(([field, fieldDef]) => {
   256	      // Skip virtual fields - they don't exist in database
   257	      if (fieldDef.virtual === true) return
   258	
   259	      // Skip hidden fields - these are NEVER returned
   260	      // Example: password_hash with hidden:true
   261	      if (fieldDef.hidden === true) return
   262	
   263	      // Skip polymorphic placeholder fields - handled via type/id columns
   264	      if (fieldDef.belongsToPolymorphic) return
   265	
   266	      // Skip normallyHidden fields - these are hidden by default
   267	      // Example: cost with normallyHidden:true (only returned when explicitly requested)
   268	      if (fieldDef.normallyHidden === true) return
   269	
   270	      fieldsToSelect.add(field)
   271	    })
   272	
   273	    // When no sparse fieldsets, we compute all computed fields
   274	    // So we need to include their dependencies even if normallyHidden
   275	    // Example: profit_margin depends on 'cost' which is normallyHidden
   276	    // We fetch 'cost' for calculation but don't return it in response
   277	    for (const [fieldName, fieldDef] of Object.entries(computedFields)) {
   278	      if (fieldDef.dependencies) {
   279	        for (const dep of fieldDef.dependencies) {
   280	          const depFieldDef = schemaStructure[dep]
   281	          if (depFieldDef && depFieldDef.hidden !== true) {
   282	            fieldsToSelect.add(dep)
   283	            // Track normallyHidden dependencies for later removal
   284	            // These are fetched for computation but not returned
   285	            if (depFieldDef.normallyHidden === true) {
   286	              computedDependencies.add(dep)
   287	            }
   288	          }
   289	        }
   290	      }
   291	    }
   292	  }
   293	
   294	  // Always include foreign keys for relationships (unless hidden)
   295	  Object.entries(schemaStructure).forEach(([field, fieldDef]) => {
   296	    if (fieldDef.belongsTo && fieldDef.hidden !== true) {
   297	      fieldsToSelect.add(field)
   298	    }
   299	  })
   300	
   301	  // Always include polymorphic type and id fields from relationships
   302	  try {
   303	    const relationships = scope.vars.schemaInfo.schemaRelationships
   304	    Object.entries(relationships || {}).forEach(([relName, relDef]) => {
   305	      if (relDef.belongsToPolymorphic) {
   306	        if (relDef.typeField) fieldsToSelect.add(relDef.typeField)
   307	        if (relDef.idField) fieldsToSelect.add(relDef.idField)
   308	      }
   309	    })
   310	  } catch (e) {
   311	    // Scope might not have relationships
   312	  }
   313	
   314	  // Return detailed information about field selection
   315	  // This info is used by:
   316	  // 1. SQL query builder to SELECT the right columns
   317	  // 2. enrichAttributes to know which computed fields to calculate
   318	  // 3. enrichAttributes to remove dependencies from final response
   319	  return {
   320	    fieldsToSelect: Array.from(fieldsToSelect),      // Fields to SELECT from database
   321	    requestedFields: requested,                       // Fields explicitly requested by user
   322	    computedDependencies: Array.from(computedDependencies),  // Dependencies to remove from response
   323	    idProperty                                        // Pass idProperty for reference
   324	  }
   325	}
   326	
   327	/**
   328	 * Determines which computed fields to calculate based on sparse fieldsets
   329	 *
   330	 * @param {string} scopeName - Resource name (e.g., 'products')
   331	 * @param {Array<string>|string} requestedFields - Requested fields from query
   332	 * @param {Object} computedFields - Computed field definitions from schema
   333	 * @returns {Array<string>} Names of computed fields to calculate
   334	 *
   335	 * @example
   336	 * // Input: No sparse fieldsets (calculate all computed fields)
   337	 * const computed = {
   338	 *   full_name: { compute: (r) => `${r.first} ${r.last}` },
   339	 *   age: { compute: (r) => new Date().getFullYear() - r.birth_year }
   340	 * };
   341	 * getRequestedComputedFields('users', null, computed);
   342	 * // Output: ['full_name', 'age'] (all computed fields)
   343	 *
   344	 * @example
   345	 * // Input: Sparse fieldsets with some computed fields
   346	 * const requestedFields = 'first_name,full_name,email';
   347	 * const computed = {
   348	 *   full_name: { compute: (r) => `${r.first} ${r.last}` },
   349	 *   age: { compute: (r) => new Date().getFullYear() - r.birth_year }
   350	 * };
   351	 * getRequestedComputedFields('users', requestedFields, computed);
   352	 * // Output: ['full_name'] (only requested computed field)
   353	 *
   354	 * @example
   355	 * // Input: Sparse fieldsets with no computed fields
   356	 * const requestedFields = ['first_name', 'email'];
   357	 * const computed = {
   358	 *   full_name: { compute: (r) => `${r.first} ${r.last}` }
   359	 * };
   360	 * getRequestedComputedFields('users', requestedFields, computed);
   361	 * // Output: [] (no computed fields requested)
   362	 *
   363	 * @description
   364	 * Used by:
   365	 * - enrichAttributes to determine which computations to run
   366	 * - Applied after fetching data from database
   367	 *
   368	 * Purpose:
   369	 * - Optimizes performance by only computing requested fields
   370	 * - Supports JSON:API sparse fieldsets for computed fields
   371	 * - Handles both string and array input formats
   372	 *
   373	 * Data flow:
   374	 * 1. Checks if sparse fieldsets are specified
   375	 * 2. If not, returns all computed field names
   376	 * 3. If yes, filters to only requested computed fields
   377	 * 4. Normalizes string input to array format
   378	 */
   379	export const getRequestedComputedFields = (scopeName, requestedFields, computedFields) => {
   380	  if (!computedFields) return []
   381	
   382	  const allComputedFields = Object.keys(computedFields)
   383	
   384	  if (!requestedFields || requestedFields.length === 0) {
   385	    // No sparse fieldsets - return all computed fields
   386	    return allComputedFields
   387	  }
   388	
   389	  // Parse requested fields if it's a string
   390	  const requested = typeof requestedFields === 'string'
   391	    ? requestedFields.split(',').map(f => f.trim()).filter(f => f)
   392	    : requestedFields
   393	
   394	  // Return only requested computed fields that exist
   395	  return requested.filter(field => allComputedFields.includes(field))
   396	}
