     1	import { RestApiValidationError } from '../../../lib/rest-api-errors.js'
     2	import { validateQueryPayload } from '../lib/querying-writing/payload-validators.js'
     3	import { normalizeRecordAttributes } from '../lib/querying-writing/database-value-normalizers.js'
     4	import { getRequestedComputedFields } from '../lib/querying-writing/knex-field-helpers.js'
     5	import { transformJsonApiToSimplified } from '../lib/querying-writing/simplified-helpers.js'
     6	import { cascadeConfig } from './common.js'
     7	
     8	/**
     9	 * QUERY
    10	 * Retrieves a collection of resources (e.g., a list of articles) based on provided criteria.
    11	 * This function sends a GET request to /api/{resourceType}.
    12	 *
    13	 * @param {string} resourceType - The type of resource collection to fetch (e.g., "articles").
    14	 * @param {object} [queryParams={}] - Optional. An object to customize the query for the collection.
    15	 * @param {string[]} [queryParams.include=[]] - An optional array of relationship paths to sideload for each resource in the collection. These paths will be converted to a comma-separated string for the URL (e.g., `['author', 'comments.user']` becomes `author,comments.user`). Supports deep relationships (e.g., "publisher.country").
    16	 * @param {object} [queryParams.fields] - An object to request only specific fields (sparse fieldsets) for each resource in the collection and its included relationships. Keys are resource types, values are comma-separated field names.
    17	 * @param {object} [queryParams.filters] - An object to filter the collection. Keys are filter parameters (specific to your API's implementation, e.g., 'status', 'title'), values are the filter criteria.
    18	 * @param {string[]} [queryParams.sort=[]] - An optional array of fields to sort the collection by. Each string represents a field; prefix with '-' for descending order (e.g., `['title', '-published-date']` becomes `title,-published-date`).
    19	 * @param {object} [queryParams.page] - An object for pagination. Typically includes `number` (page number) and `size` (items per page). E.g., `{ number: 1, size: 10 }`.
    20	 * @returns {Promise<object>} A Promise that resolves to the JSON:API response document containing the resource collection.
    21	 */
    22	export default async function queryMethod ({
    23	  params,
    24	  context,
    25	  vars,
    26	  helpers,
    27	  scope,
    28	  scopes,
    29	  runHooks,
    30	  apiOptions,
    31	  pluginOptions,
    32	  scopeOptions,
    33	  scopeName,
    34	  log,
    35	  api
    36	}) {
    37	  context.method = 'query'
    38	
    39	  // Determine which simplified setting to use based on transport
    40	  const isTransport = params.isTransport === true
    41	
    42	  // Use vars which automatically cascade from scope to global
    43	  const defaultSimplified = isTransport ? vars.simplifiedTransport : vars.simplifiedApi
    44	
    45	  // Get simplified setting - from params only (per-call override) or use default
    46	  context.simplified = params.simplified !== undefined ? params.simplified : defaultSimplified
    47	
    48	  // Assign common context properties
    49	  context.schemaInfo = scopes[scopeName].vars.schemaInfo // This is the object variable created by compileSchemas
    50	  context.queryParams = params.queryParams || {}
    51	
    52	  // These only make sense as parameter per query
    53	  context.queryParams.fields = cascadeConfig('fields', [params.queryParams], {})
    54	  context.queryParams.include = cascadeConfig('include', [params.queryParams], [])
    55	  context.queryParams.sort = cascadeConfig('sort', [params.queryParams], [])
    56	  context.queryParams.page = cascadeConfig('page', [params.queryParams], {})
    57	
    58	  context.transaction = params.transaction
    59	  context.db = context.transaction || api.knex.instance
    60	
    61	  context.scopeName = scopeName
    62	
    63	  // These are just shortcuts used in this function and will be returned
    64	  const searchSchema = context.schemaInfo.searchSchemaInstance
    65	  const schemaStructure = context.schemaInfo.schemaInstance.structure
    66	  const schemaRelationships = context.schemaInfo.schemaRelationships
    67	
    68	  // Sortable fields and sort (mab)
    69	  context.sortableFields = vars.sortableFields
    70	  // Apply default sort if no sort specified
    71	  if (context.queryParams.sort.length === 0 && vars.defaultSort) {
    72	    context.queryParams.sort = Array.isArray(vars.defaultSort) ? vars.defaultSort : [vars.defaultSort]
    73	  }
    74	
    75	  // Validate query parameters to ensure they follow JSON:API specification and security rules.
    76	  // This checks that filters are valid field names, sort fields exist in sortableFields array
    77	  // (preventing SQL injection), pagination uses valid page[size]/page[number] format, and include
    78	  // paths reference real relationships. Example: sort: ['-createdAt', 'title'] is checked against
    79	  // sortableFields to ensure users can't sort by sensitive fields like 'password_hash'.
    80	  validateQueryPayload({ queryParams: context.queryParams }, context.sortableFields, vars.includeDepthLimit)
    81	
    82	  // Validate search/filter parameters against searchSchema
    83	  if (context.queryParams.filters && Object.keys(context.queryParams.filters).length > 0) {
    84	    // Only allow filtering if searchSchema is defined
    85	    if (!searchSchema) {
    86	      throw new RestApiValidationError(
    87	        `Filtering is not enabled for resource '${scopeName}'. To enable filtering, add 'search: true' to schema fields or define a searchSchema.`,
    88	        {
    89	          fields: Object.keys(context.queryParams.filters).map(field => `filters.${field}`),
    90	          violations: [{
    91	            field: 'filters',
    92	            rule: 'filtering_not_enabled',
    93	            message: 'Resource does not have searchable fields defined'
    94	          }]
    95	        }
    96	      )
    97	    }
    98	
    99	    // Validate the filter parameters searchSchema
   100	    const { validatedObject, errors } = await searchSchema.validate(context.queryParams.filters, {
   101	      onlyObjectValues: true // Partial validation for filters
   102	    })
   103	
   104	    // If there are validation errors, throw an error
   105	    if (Object.keys(errors).length > 0) {
   106	      const violations = Object.entries(errors).map(([field, error]) => ({
   107	        field: `filters.${field}`,
   108	        rule: error.code || 'invalid_value',
   109	        message: error.message
   110	      }))
   111	
   112	      throw new RestApiValidationError(
   113	        'Invalid filter parameters',
   114	        {
   115	          fields: Object.keys(errors).map(field => `filters.${field}`),
   116	          violations
   117	        }
   118	      )
   119	    }
   120	
   121	    // Replace filter with validated/transformed values
   122	    context.queryParams.filters = validatedObject
   123	  }
   124	
   125	  // Centralised checkPermissions function
   126	  await scope.checkPermissions({
   127	    method: 'query',
   128	    originalContext: context,
   129	  })
   130	
   131	  await runHooks('beforeData')
   132	  await runHooks('beforeDataQuery')
   133	  context.record = await helpers.dataQuery({
   134	    scopeName,
   135	    context,
   136	    runHooks
   137	  })
   138	
   139	  // Normalize database values (e.g., convert 1/0 to true/false for booleans)
   140	  context.record = normalizeRecordAttributes(context.record, scopes)
   141	
   142	  // Make a backup
   143	  try {
   144	    context.originalRecord = structuredClone(context.record)
   145	  } catch (e) {
   146	    log.error('Failed to clone record:', {
   147	      error: e.message,
   148	      recordKeys: Object.keys(context.record || {}),
   149	      hasHttpRequest: !!context.raw?.req
   150	    })
   151	    throw e
   152	  }
   153	
   154	  // This will enhance record, which is the WHOLE JSON:API record
   155	  await runHooks('enrichRecord')
   156	
   157	  // Get computed field information for main resource
   158	  const computedFields = scope.vars.schemaInfo?.computed || {}
   159	  const requestedFields = context.queryParams.fields?.[scopeName]
   160	  const requestedComputedFields = getRequestedComputedFields(scopeName, requestedFields, computedFields)
   161	
   162	  // Run enrichAttributes for every single set of attribute, calling it from the right scope
   163	  for (const entry of context.record.data) {
   164	    entry.attributes = await scope.enrichAttributes({
   165	      attributes: entry.attributes,
   166	      parentContext: context,
   167	      requestedComputedFields,
   168	      isMainResource: true,
   169	      computedDependencies: context.computedDependencies
   170	    })
   171	  }
   172	  for (const entry of (context.record.included || [])) {
   173	    const entryScope = scopes[entry.type]
   174	    const entryComputed = entryScope.vars.schemaInfo?.computed || {}
   175	    const entryRequestedFields = context.queryParams.fields?.[entry.type]
   176	    const entryRequestedComputed = getRequestedComputedFields(
   177	      entry.type,
   178	      entryRequestedFields,
   179	      entryComputed
   180	    )
   181	
   182	    entry.attributes = await entryScope.enrichAttributes({
   183	      attributes: entry.attributes,
   184	      parentContext: context,
   185	      requestedComputedFields: entryRequestedComputed,
   186	      isMainResource: false,
   187	      computedDependencies: entry.__$jsonrestapi_computed_deps$__
   188	    })
   189	  }
   190	
   191	  // The called hooks should NOT change context.record
   192	  await runHooks('finish')
   193	  await runHooks('finishQuery')
   194	
   195	  // Transform output if in simplified mode
   196	  if (context.simplified) {
   197	    // Convert JSON:API response back to simplified format
   198	    // Example: {data: {type: 'posts', id: '1', attributes: {title: 'My Post'}, relationships: {author: {data: {type: 'users', id: '123'}}}}}
   199	    // becomes: {id: '1', title: 'My Post', author_id: '123'} - flattens structure and restores foreign keys
   200	    return transformJsonApiToSimplified(
   201	      { record: context.record },
   202	      { context: { schemaStructure, schemaRelationships, scopes } }
   203	    )
   204	  }
   205	
   206	  return context.record
   207	}
