     1	/**
     2	 * Schema processing utilities for search and field dependencies
     3	 *
     4	 * @description
     5	 * This module provides utilities for:
     6	 * - Marking search fields as indexed for database optimization
     7	 * - Generating search schemas from various configuration sources
     8	 * - Sorting fields by their dependencies (for getters/setters)
     9	 * - Detecting circular dependencies in field definitions
    10	 */
    11	
    12	import { RestApiValidationError } from '../../../../lib/rest-api-errors.js'
    13	
    14	/**
    15	 * Marks all fields in a searchSchema as indexed for database optimization
    16	 *
    17	 * @param {Object} searchSchema - Search schema object to process
    18	 * @returns {void} Modifies the searchSchema in-place
    19	 *
    20	 * @example
    21	 * // Input: Search fields without index flags
    22	 * const searchSchema = {
    23	 *   title: { type: 'string', filterOperator: 'contains' },
    24	 *   status: { type: 'string', filterOperator: '=' },
    25	 *   created_at: { type: 'datetime', filterOperator: '>=' }
    26	 * };
    27	 * ensureSearchFieldsAreIndexed(searchSchema);
    28	 *
    29	 * // Output: All fields marked as indexed
    30	 * // {
    31	 * //   title: { type: 'string', filterOperator: 'contains', indexed: true },
    32	 * //   status: { type: 'string', filterOperator: '=', indexed: true },
    33	 * //   created_at: { type: 'datetime', filterOperator: '>=', indexed: true }
    34	 * // }
    35	 *
    36	 * @example
    37	 * // Input: Virtual field for cross-table search
    38	 * const searchSchema = {
    39	 *   name: { type: 'string', filterOperator: 'contains' },
    40	 *   author_name: {                              // Virtual field
    41	 *     type: 'string',
    42	 *     filterOperator: 'contains',
    43	 *     virtualField: {
    44	 *       joinTo: 'users',
    45	 *       joinOn: ['author_id', 'id'],
    46	 *       searchField: 'name'
    47	 *     }
    48	 *   }
    49	 * };
    50	 * ensureSearchFieldsAreIndexed(searchSchema);
    51	 *
    52	 * // Output: Both regular and virtual fields indexed
    53	 * // {
    54	 * //   name: { ..., indexed: true },
    55	 * //   author_name: { ..., indexed: true }  // Enables efficient JOIN
    56	 * // }
    57	 *
    58	 * @example
    59	 * // Input: Already indexed or explicitly false
    60	 * const searchSchema = {
    61	 *   email: { type: 'string', indexed: false },   // Explicitly false
    62	 *   username: { type: 'string', indexed: true }   // Already true
    63	 * };
    64	 * ensureSearchFieldsAreIndexed(searchSchema);
    65	 *
    66	 * // Output: All forced to indexed: true
    67	 * // {
    68	 * //   email: { type: 'string', indexed: true },    // Overridden
    69	 * //   username: { type: 'string', indexed: true }   // Unchanged
    70	 * // }
    71	 *
    72	 * @description
    73	 * Used by:
    74	 * - rest-api-plugin during scope initialization
    75	 * - Applied to all searchSchema fields before storage setup
    76	 *
    77	 * Purpose:
    78	 * - Signals storage plugins which fields need database indexes
    79	 * - Enables efficient filtering without full table scans
    80	 * - Supports cross-table searches via indexed JOINs
    81	 * - Improves query performance for filtered API requests
    82	 *
    83	 * Data flow:
    84	 * 1. Receives searchSchema object (or null)
    85	 * 2. Iterates through each field definition
    86	 * 3. Sets indexed: true on all fields
    87	 * 4. Storage plugins use this flag for index creation
    88	 */
    89	export function ensureSearchFieldsAreIndexed (searchSchema) {
    90	  if (!searchSchema) return
    91	
    92	  Object.keys(searchSchema).forEach(fieldName => {
    93	    const fieldDef = searchSchema[fieldName]
    94	    if (fieldDef && typeof fieldDef === 'object') {
    95	      // Mark the field as indexed for cross-table search support
    96	      fieldDef.indexed = true
    97	    }
    98	  })
    99	}
   100	
   101	/**
   102	 * Generates complete searchSchema by merging schema search definitions with explicit searchSchema
   103	 *
   104	 * @param {Object} schema - Main resource schema with optional 'search' properties
   105	 * @param {Object} explicitSearchSchema - Optional explicit searchSchema to merge
   106	 * @returns {Object|null} Merged searchSchema or null if no search fields
   107	 * @throws {RestApiValidationError} If same field defined in multiple places
   108	 *
   109	 * @example
   110	 * // Input: Simple search fields in schema
   111	 * const schema = {
   112	 *   title: { type: 'string', search: true },              // Simple
   113	 *   content: { type: 'text' },                            // Not searchable
   114	 *   status: { type: 'string', search: { filterOperator: '=' } }  // Custom
   115	 * };
   116	 * const result = generateSearchSchemaFromSchema(schema, null);
   117	 *
   118	 * // Output: Generated search schema
   119	 * // {
   120	 * //   title: { type: 'string' },                           // No operator set; resolver applies defaults
   121	 * //   status: { type: 'string', filterOperator: '=' }      // Specified operator
   122	 * // }
   123	 *
   124	 * @example
   125	 * // Input: Multiple filters from one field
   126	 * const schema = {
   127	 *   published_at: {
   128	 *     type: 'datetime',
   129	 *     search: {
   130	 *       published_after: { filterOperator: '>=', type: 'datetime' },
   131	 *       published_before: { filterOperator: '<=', type: 'datetime' }
   132	 *     }
   133	 *   }
   134	 * };
   135	 * const result = generateSearchSchemaFromSchema(schema, null);
   136	 *
   137	 * // Output: Two search fields from one database field
   138	 * // {
   139	 * //   published_after: {
   140	 * //     type: 'datetime',
   141	 * //     actualField: 'published_at',      // Maps to real field
   142	 * //     filterOperator: '>='
   143	 * //   },
   144	 * //   published_before: {
   145	 * //     type: 'datetime',
   146	 * //     actualField: 'published_at',      // Same field, different operator
   147	 * //     filterOperator: '<='
   148	 * //   }
   149	 * // }
   150	 *
   151	 * @example
   152	 * // Input: Merging with explicit searchSchema
   153	 * const schema = {
   154	 *   name: { type: 'string', search: true }
   155	 * };
   156	 * const explicitSearchSchema = {
   157	 *   email: { type: 'string', filterOperator: '=' },
   158	 *   age: { type: 'number', filterOperator: '>=' }
   159	 * };
   160	 * const result = generateSearchSchemaFromSchema(schema, explicitSearchSchema);
   161	 *
   162	 * // Output: Combined from both sources
   163	 * // {
   164	 * //   email: { type: 'string', filterOperator: '=' },    // Explicit
   165	 * //   age: { type: 'number', filterOperator: '>=' },     // Explicit
   166	 * //   name: { type: 'string' }                           // From schema (no operator set)
   167	 * // }
   168	 *
   169	 * @example
   170	 * // Input: Explicit searchSchema takes precedence
   171	 * const schema = {
   172	 *   email: { type: 'string', search: true }
   173	 * };
   174	 * const explicitSearchSchema = {
   175	 *   email: { type: 'string', filterOperator: 'contains' }
   176	 * };
   177	 * const result = generateSearchSchemaFromSchema(schema, explicitSearchSchema);
   178	 * // Output: Explicit searchSchema wins
   179	 * // {
   180	 * //   email: { type: 'string', filterOperator: 'contains' }  // Uses explicit definition
   181	 * // }
   182	 *
   183	 * @example
   184	 * // Input: Virtual search fields
   185	 * const schema = {
   186	 *   title: { type: 'string' },
   187	 *   _virtual: {
   188	 *     search: {
   189	 *       category_name: {                    // Doesn't exist in DB
   190	 *         type: 'string',
   191	 *         filterOperator: 'contains',
   192	 *         virtualField: {
   193	 *           joinTo: 'categories',
   194	 *           joinOn: ['category_id', 'id'], // JOIN condition
   195	 *           searchField: 'name'             // Field in related table
   196	 *         }
   197	 *       }
   198	 *     }
   199	 *   }
   200	 * };
   201	 *
   202	 * // Output: Virtual field for cross-table search
   203	 * // {
   204	 * //   category_name: {
   205	 * //     type: 'string',
   206	 * //     filterOperator: 'contains',
   207	 * //     virtualField: { ... join config ... }
   208	 * //   }
   209	 * // }
   210	 *
   211	 * @description
   212	 * Used by:
   213	 * - rest-api-plugin during scope initialization
   214	 * - Generates searchSchema from various sources
   215	 *
   216	 * Purpose:
   217	 * - Provides flexible search configuration options
   218	 * - Enables virtual fields for cross-table searches
   219	 * - Supports range queries (before/after patterns)
   220	 * - Merges search:true fields with explicit searchSchema
   221	 * - Explicit searchSchema always takes precedence
   222	 * - Allows storage plugins to optimize queries
   223	 *
   224	 * Data flow:
   225	 * 1. Starts with explicit searchSchema (if any)
   226	 * 2. Processes schema fields with 'search' property
   227	 * 3. Skips fields already in explicit searchSchema (no conflicts)
   228	 * 4. Handles multiple filters from single field
   229	 * 5. Processes virtual search definitions
   230	 * 6. Returns merged searchSchema or null
   231	 */
   232	export const generateSearchSchemaFromSchema = (schema, explicitSearchSchema) => {
   233	  // Start with explicit searchSchema or empty object
   234	  const searchSchema = explicitSearchSchema ? { ...explicitSearchSchema } : {}
   235	
   236	  if (!schema) {
   237	    return Object.keys(searchSchema).length > 0 ? searchSchema : null
   238	  }
   239	
   240	  // Process schema fields with 'search' property
   241	  Object.entries(schema).forEach(([fieldName, fieldDef]) => {
   242	    const effectiveSearch = fieldDef.search
   243	
   244	    if (effectiveSearch) {
   245	      if (effectiveSearch === true) {
   246	        // For belongsTo relationships, use the relationship name (as property) instead of the field name
   247	        const searchFieldName = (fieldDef.belongsTo && fieldDef.as) ? fieldDef.as : fieldName
   248	
   249	        // Check if field already exists in explicit searchSchema
   250	        if (searchSchema[searchFieldName]) {
   251	          // Skip - explicit searchSchema takes precedence
   252	          // This allows searchSchema to override fields marked with search:true
   253	          return
   254	        }
   255	
   256	        // Simple boolean - copy entire field definition (except search)
   257	        const { search, ...fieldDefWithoutSearch } = fieldDef
   258	        const searchEntry = {
   259	          ...fieldDefWithoutSearch,
   260	          // Do not set filterOperator here; centralized resolver will apply sensible defaults
   261	        }
   262	
   263	        // For belongsTo relationships, add metadata to map back to the actual field
   264	        if (fieldDef.belongsTo && fieldDef.as) {
   265	          searchEntry.actualField = fieldName  // Maps to the DB column
   266	          searchEntry.isRelationship = true
   267	          searchEntry.targetResource = fieldDef.belongsTo
   268	        }
   269	
   270	        searchSchema[searchFieldName] = searchEntry
   271	      } else if (typeof effectiveSearch === 'object') {
   272	        // Check if search defines multiple filter fields
   273	        const hasNestedFilters = Object.values(effectiveSearch).some(
   274	          v => typeof v === 'object' && v.filterOperator
   275	        )
   276	
   277	        if (hasNestedFilters) {
   278	          // Multiple filters from one field (like published_after/before)
   279	          Object.entries(effectiveSearch).forEach(([filterName, filterDef]) => {
   280	            // Check if filter already exists in explicit searchSchema
   281	            if (searchSchema[filterName]) {
   282	              // Skip - explicit searchSchema takes precedence
   283	              return
   284	            }
   285	
   286	            searchSchema[filterName] = {
   287	              type: fieldDef.type,
   288	              actualField: fieldName,
   289	              ...filterDef
   290	            }
   291	          })
   292	        } else {
   293	          // For belongsTo relationships, use the relationship name (as property) instead of the field name
   294	          const searchFieldName = (fieldDef.belongsTo && fieldDef.as) ? fieldDef.as : fieldName
   295	
   296	          // Check if field already exists in explicit searchSchema
   297	          if (searchSchema[searchFieldName]) {
   298	            // Skip - explicit searchSchema takes precedence
   299	            return
   300	          }
   301	
   302	          // Single filter with config
   303	          const searchEntry = {
   304	            type: fieldDef.type,
   305	            ...effectiveSearch
   306	          }
   307	
   308	          // For belongsTo relationships, add metadata to map back to the actual field
   309	          if (fieldDef.belongsTo && fieldDef.as) {
   310	            searchEntry.actualField = fieldName  // Maps to the DB column
   311	            searchEntry.isRelationship = true
   312	            searchEntry.targetResource = fieldDef.belongsTo
   313	          }
   314	
   315	          searchSchema[searchFieldName] = searchEntry
   316	        }
   317	      }
   318	    }
   319	  })
   320	
   321	  // Handle _virtual search definitions
   322	  if (schema._virtual?.search) {
   323	    Object.entries(schema._virtual.search).forEach(([filterName, filterDef]) => {
   324	      // Check if filter already exists in explicit searchSchema
   325	      if (searchSchema[filterName]) {
   326	        // Skip - explicit searchSchema takes precedence
   327	        return
   328	      }
   329	
   330	      searchSchema[filterName] = filterDef
   331	    })
   332	  }
   333	
   334	  return Object.keys(searchSchema).length > 0 ? searchSchema : null
   335	}
   336	
   337	/**
   338	 * Generic topological sort for handling dependencies
   339	 *
   340	 * @param {Array} items - Array of items to sort
   341	 * @param {Function} getDependencies - Function that returns dependencies for an item
   342	 * @returns {Array} Sorted array respecting dependencies
   343	 * @throws {Error} If circular dependencies or unknown dependencies detected
   344	 *
   345	 * @example
   346	 * // Input: Simple dependency chain
   347	 * const items = ['a', 'b', 'c'];
   348	 * const deps = { a: ['b'], b: ['c'], c: [] };
   349	 * const sorted = topologicalSort(items, item => deps[item]);
   350	 * // Output: ['c', 'b', 'a']
   351	 * // c first (no deps), then b (depends on c), then a (depends on b)
   352	 *
   353	 * @example
   354	 * // Input: Circular dependency
   355	 * const items = ['a', 'b'];
   356	 * const deps = { a: ['b'], b: ['a'] };  // Circular!
   357	 * topologicalSort(items, item => deps[item]);
   358	 * // Throws: Error "Circular dependency detected: b"
   359	 *
   360	 * @example
   361	 * // Input: Unknown dependency
   362	 * const items = ['a', 'b'];
   363	 * const deps = { a: ['c'], b: [] };     // 'c' not in items
   364	 * topologicalSort(items, item => deps[item]);
   365	 * // Throws: Error "Unknown dependency 'c' for 'a'"
   366	 *
   367	 * @description
   368	 * Used by:
   369	 * - sortFieldsByDependencies for ordering field operations
   370	 * - Any code needing dependency-based ordering
   371	 *
   372	 * Purpose:
   373	 * - Orders items so dependencies come before dependents
   374	 * - Detects circular dependencies early
   375	 * - Validates all dependencies exist
   376	 * - Uses depth-first search algorithm
   377	 */
   378	export function topologicalSort (items, getDependencies) {
   379	  const sorted = []
   380	  const visited = new Set()
   381	  const visiting = new Set()
   382	
   383	  function visit (item) {
   384	    if (visited.has(item)) return
   385	
   386	    if (visiting.has(item)) {
   387	      throw new Error(`Circular dependency detected: ${item}`)
   388	    }
   389	
   390	    visiting.add(item)
   391	
   392	    const dependencies = getDependencies(item) || []
   393	    for (const dep of dependencies) {
   394	      if (!items.includes(dep)) {
   395	        throw new Error(`Unknown dependency '${dep}' for '${item}'`)
   396	      }
   397	      visit(dep)
   398	    }
   399	
   400	    visiting.delete(item)
   401	    visited.add(item)
   402	    sorted.push(item)
   403	  }
   404	
   405	  for (const item of items) {
   406	    visit(item)
   407	  }
   408	
   409	  return sorted
   410	}
   411	
   412	/**
   413	 * Sorts fields by their dependencies using topological sort
   414	 *
   415	 * @param {Object} fields - Object with field definitions
   416	 * @param {string} dependencyProperty - Property name containing dependencies
   417	 * @returns {Array} Field names sorted by dependencies
   418	 * @throws {Error} If circular dependencies or unknown fields detected
   419	 *
   420	 * @example
   421	 * // Input: Getter dependencies (fullName needs firstName and lastName)
   422	 * const fields = {
   423	 *   firstName: { getter: v => v.trim() },
   424	 *   lastName: { getter: v => v.trim() },
   425	 *   fullName: {
   426	 *     getter: (v, ctx) => `${ctx.attributes.firstName} ${ctx.attributes.lastName}`,
   427	 *     runGetterAfter: ['firstName', 'lastName']
   428	 *   }
   429	 * };
   430	 * const sorted = sortFieldsByDependencies(fields, 'runGetterAfter');
   431	 * // Output: ['firstName', 'lastName', 'fullName']
   432	 * // Ensures firstName/lastName getters run before fullName
   433	 *
   434	 * @example
   435	 * // Input: Complex dependency chain
   436	 * const fields = {
   437	 *   a: { runGetterAfter: ['b', 'c'] },    // a needs b and c
   438	 *   b: { runGetterAfter: ['d'] },         // b needs d
   439	 *   c: { runGetterAfter: ['d'] },         // c needs d
   440	 *   d: { runGetterAfter: [] }             // d needs nothing
   441	 * };
   442	 * const sorted = sortFieldsByDependencies(fields, 'runGetterAfter');
   443	 * // Output: ['d', 'b', 'c', 'a']
   444	 * // d first, then b/c (both need d), then a (needs b/c)
   445	 *
   446	 * @example
   447	 * // Input: Circular dependency error
   448	 * const fields = {
   449	 *   a: { runGetterAfter: ['b'] },
   450	 *   b: { runGetterAfter: ['a'] }          // Circular!
   451	 * };
   452	 * sortFieldsByDependencies(fields, 'runGetterAfter');
   453	 * // Throws: Error "Circular dependency detected: a in runGetterAfter"
   454	 *
   455	 * @description
   456	 * Used by:
   457	 * - Schema processing for getter/setter ordering
   458	 * - Ensures dependent fields process after dependencies
   459	 *
   460	 * Purpose:
   461	 * - Orders field operations by dependencies
   462	 * - Enables computed fields that depend on other fields
   463	 * - Validates dependency graph is acyclic
   464	 * - Provides clear error messages for debugging
   465	 *
   466	 * Data flow:
   467	 * 1. Extracts field names from object
   468	 * 2. Calls topologicalSort with dependency function
   469	 * 3. Returns ordered field names
   470	 * 4. Enhances error messages with property name
   471	 */
   472	export function sortFieldsByDependencies (fields, dependencyProperty) {
   473	  const fieldNames = Object.keys(fields)
   474	
   475	  if (fieldNames.length === 0) return []
   476	
   477	  try {
   478	    return topologicalSort(fieldNames, (fieldName) => {
   479	      const field = fields[fieldName]
   480	      return field[dependencyProperty] || []
   481	    })
   482	  } catch (error) {
   483	    if (error.message.includes('Circular dependency')) {
   484	      throw new Error(`${error.message} in ${dependencyProperty}`)
   485	    }
   486	    throw error
   487	  }
   488	}
