     1	import { RestApiResourceError } from '../../../lib/rest-api-errors.js'
     2	import { validatePatchPayload } from '../lib/querying-writing/payload-validators.js'
     3	import { processRelationships } from '../lib/writing/relationship-processor.js'
     4	import { updateManyToManyRelationship } from '../lib/writing/many-to-many-manipulations.js'
     5	import { ERROR_SUBTYPES } from '../lib/querying-writing/knex-constants.js'
     6	import {
     7	  setupCommonRequest,
     8	  validateResourceAttributesBeforeWrite,
     9	  validateRelationshipAccess,
    10	  applyFieldSetters,
    11	  validatePivotResource,
    12	  handleRecordReturnAfterWrite,
    13	  handleWriteMethodError
    14	} from './common.js'
    15	
    16	/**
    17	 * PATCH
    18	 * Performs a partial update on an existing resource's attributes or relationships.
    19	 * Unlike PUT, PATCH only updates the fields provided, leaving other fields unchanged.
    20	 * This method supports updating both attributes and relationships (1:1 and n:n).
    21	 * For relationships, only the ones explicitly provided will be updated.
    22	 * Just like PUT, it CANNOT have the `included` array in data.
    23	 */
    24	export default async function patchMethod ({
    25	  params,
    26	  context,
    27	  vars,
    28	  helpers,
    29	  scope,
    30	  scopes,
    31	  runHooks,
    32	  apiOptions,
    33	  pluginOptions,
    34	  scopeOptions,
    35	  scopeName,
    36	  api,
    37	  log
    38	}) {
    39	  context.method = 'patch'
    40	
    41	  try {
    42	    const { schema, schemaStructure, schemaRelationships } = await setupCommonRequest({
    43	      params,
    44	      context,
    45	      vars,
    46	      scopes,
    47	      scopeOptions,
    48	      scopeName,
    49	      api,
    50	      helpers
    51	    })
    52	    context.id = context.inputRecord.data.id
    53	
    54	    // Run early hooks for pre-processing (e.g., file handling)
    55	    await runHooks('beforeProcessing')
    56	    await runHooks('beforeProcessingPatch')
    57	
    58	    // Validate PATCH payload to ensure the partial update actually contains changes.
    59	    // PATCH requests must include either attributes to update or relationships to modify -
    60	    // an empty PATCH is invalid. This prevents accidental no-op requests and ensures clients
    61	    // are explicit about what they want to change. Unlike PUT, PATCH preserves all fields
    62	    // not mentioned in the request.
    63	    // Example: data must have either attributes: {title: 'New'} or relationships: {author: {...}}
    64	    validatePatchPayload(context.inputRecord, scopes)
    65	
    66	    // Validate that user has read access to all related resources
    67	    // This ensures users can only create relationships to resources they can access
    68	    await validateRelationshipAccess(context, context.inputRecord, helpers, runHooks, api)
    69	
    70	    // Extract foreign keys from JSON:API relationships and prepare many-to-many operations
    71	    // Example: relationships.author -> author_id: '123' for storage
    72	    // Example: relationships.tags -> array of pivot records to create later (only for provided relationships in PATCH)
    73	    const { belongsToUpdates, manyToManyRelationships } = processRelationships(
    74	      scope,
    75	      { context }
    76	    )
    77	
    78	    await validateResourceAttributesBeforeWrite({
    79	      context,
    80	      schema,
    81	      belongsToUpdates,
    82	      runHooks,
    83	      isPartialValidation: true
    84	    })
    85	
    86	    // Fetch minimal record for authorization checks
    87	    const minimalRecord = await helpers.dataGetMinimal({
    88	      scopeName,
    89	      context,
    90	      runHooks
    91	    })
    92	
    93	    if (!minimalRecord) {
    94	      throw new RestApiResourceError(
    95	        `Resource not found: ${scopeName}/${context.id}`,
    96	        ERROR_SUBTYPES.NOT_FOUND
    97	      )
    98	    }
    99	
   100	    context.minimalRecord = minimalRecord
   101	
   102	    // Centralised checkPermissions function
   103	    await scope.checkPermissions({
   104	      method: 'patch',
   105	      originalContext: context,
   106	    })
   107	
   108	    // Merge belongsTo updates into attributes before patching the record
   109	    if (Object.keys(belongsToUpdates).length > 0) {
   110	      context.inputRecord.data.attributes = {
   111	        ...context.inputRecord.data.attributes,
   112	        ...belongsToUpdates
   113	      }
   114	    }
   115	
   116	    await runHooks('beforeDataCall')
   117	    await runHooks('beforeDataCallPatch')
   118	
   119	    // Apply field setters after validation and before storage
   120	    if (context.inputRecord?.data?.attributes) {
   121	      context.inputRecord.data.attributes = await applyFieldSetters(
   122	        context.inputRecord.data.attributes,
   123	        context.schemaInfo,
   124	        context,
   125	        api,
   126	        helpers
   127	      )
   128	    }
   129	
   130	    // Call the storage helper - should return the patched record
   131	    await helpers.dataPatch({
   132	      scopeName,
   133	      context
   134	    })
   135	
   136	    await runHooks('afterDataCallPatch')
   137	    await runHooks('afterDataCall')
   138	
   139	    // Process many-to-many relationships after main record update
   140	    // For PATCH, we only update the relationships that were explicitly provided
   141	    for (const { relName, relDef, relData } of manyToManyRelationships) {
   142	      if (relDef?.through && api.anyapi?.links?.syncMany) {
   143	        await api.anyapi.links.syncMany({
   144	          context,
   145	          scopeName,
   146	          relName,
   147	          relDef,
   148	          relData,
   149	          isUpdate: true,
   150	        })
   151	        continue
   152	      }
   153	
   154	      validatePivotResource(scopes, relDef, relName)
   155	
   156	      await updateManyToManyRelationship(null, {
   157	        api,
   158	        context: {
   159	          resourceId: context.id,
   160	          relDef,
   161	          relData,
   162	          transaction: context.transaction
   163	        }
   164	      })
   165	    }
   166	
   167	    const ret = await handleRecordReturnAfterWrite({
   168	      context,
   169	      scopeName,
   170	      api,
   171	      scopes,
   172	      schemaStructure,
   173	      schemaRelationships,
   174	      scopeOptions,
   175	      vars,
   176	      runHooks,
   177	      helpers,
   178	      log
   179	    })
   180	
   181	    // Commit transaction if we created it
   182	    if (context.shouldCommit) {
   183	      await context.transaction.commit()
   184	      await runHooks('afterCommit')
   185	    }
   186	
   187	    return ret
   188	  } catch (error) {
   189	    await handleWriteMethodError(error, context, 'PATCH', scopeName, log, runHooks)
   190	  }
   191	}
