     1	import { RestApiResourceError } from '../../../lib/rest-api-errors.js'
     2	import { validatePutPayload } from '../lib/querying-writing/payload-validators.js'
     3	import { processRelationships } from '../lib/writing/relationship-processor.js'
     4	import { updateManyToManyRelationship, createPivotRecords } 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	 * PUT
    18	 * Updates an existing top-level resource by completely replacing it.
    19	 * This method supports updating both attributes and relationships (1:1 and n:n).
    20	 * PUT is a complete replacement - any relationships not provided will be removed/nulled.
    21	 * This method does NOT support creating new related resources via an `included` array.
    22	 */
    23	export default async function putMethod ({
    24	  params,
    25	  context,
    26	  vars,
    27	  helpers,
    28	  scope,
    29	  scopes,
    30	  runHooks,
    31	  apiOptions,
    32	  pluginOptions,
    33	  scopeOptions,
    34	  scopeName,
    35	  api,
    36	  log
    37	}) {
    38	  context.method = 'put'
    39	
    40	  try {
    41	    const { schema, schemaStructure, schemaRelationships } = await setupCommonRequest({
    42	      params,
    43	      context,
    44	      vars,
    45	      scopes,
    46	      scopeOptions,
    47	      scopeName,
    48	      api,
    49	      helpers
    50	    })
    51	    context.id = context.inputRecord.data.id
    52	
    53	    // Run early hooks for pre-processing (e.g., file handling)
    54	    await runHooks('beforeProcessing')
    55	    await runHooks('beforeProcessingPut')
    56	
    57	    // Validate PUT payload to ensure it's a complete resource replacement operation.
    58	    // PUT requires the full resource representation including ID (unlike POST which generates ID).
    59	    // It validates that data.id matches the URL parameter, prevents 'included' array (which is
    60	    // read-only), and ensures the payload represents a complete replacement. Any fields not
    61	    // provided will be removed or reset to defaults - this is the key difference from PATCH.
    62	    // Example: PUT to /articles/123 must have data.id: '123' and all required fields.
    63	    validatePutPayload(context.inputRecord, scopes)
    64	
    65	    // Validate that user has read access to all related resources
    66	    // This ensures users can only create relationships to resources they can access
    67	    await validateRelationshipAccess(context, context.inputRecord, helpers, runHooks, api)
    68	
    69	    // Extract foreign keys from JSON:API relationships and prepare many-to-many operations
    70	    // Example: relationships.author -> author_id: '123' for storage
    71	    // Example: relationships.tags -> array of pivot records to create later
    72	    const { belongsToUpdates, manyToManyRelationships } = processRelationships(
    73	      scope,
    74	      { context }
    75	    )
    76	
    77	    await validateResourceAttributesBeforeWrite({
    78	      context,
    79	      schema,
    80	      belongsToUpdates,
    81	      runHooks
    82	    })
    83	
    84	    // Check existence first
    85	    context.exists = await helpers.dataExists({
    86	      scopeName,
    87	      context
    88	    })
    89	
    90	    context.isCreate = !context.exists
    91	    context.isUpdate = context.exists
    92	
    93	    // Fetch minimal record for authorization checks (only for updates)
    94	    if (context.isUpdate) {
    95	      const minimalRecord = await helpers.dataGetMinimal({
    96	        scopeName,
    97	        context,
    98	        runHooks
    99	      })
   100	
   101	      if (!minimalRecord) {
   102	        throw new RestApiResourceError(
   103	          `Resource not found: ${scopeName}/${context.id}`,
   104	          ERROR_SUBTYPES.NOT_FOUND
   105	        )
   106	      }
   107	
   108	      context.minimalRecord = minimalRecord
   109	    }
   110	
   111	    // For PUT, we also need to handle relationships that are NOT provided
   112	    // (they should be set to null/empty as PUT is a complete replacement)
   113	    const allRelationships = {}
   114	
   115	    // Collect all defined relationships for this resource
   116	    for (const [relName, relDef] of Object.entries(schemaRelationships || {})) {
   117	      if (relDef.type === 'manyToMany') {
   118	        allRelationships[relName] = {
   119	          type: 'manyToMany',
   120	          relDef: {
   121	            through: relDef.through,
   122	            foreignKey: relDef.foreignKey,
   123	            otherKey: relDef.otherKey
   124	          }
   125	        }
   126	      }
   127	    }
   128	
   129	    // Also check schema fields for belongsTo relationships
   130	    for (const [fieldName, fieldDef] of Object.entries(schemaStructure)) {
   131	      if (fieldDef.as && fieldDef.belongsTo) {
   132	        allRelationships[fieldDef.as] = {
   133	          type: 'belongsTo',
   134	          fieldName,
   135	          fieldDef
   136	        }
   137	      }
   138	    }
   139	
   140	    // Process missing relationships (PUT should null them out only if relationships object exists)
   141	    const hasRelationshipsObject = context.inputRecord.data.relationships !== undefined
   142	    const providedRelationships = new Set(Object.keys(context.inputRecord.data.relationships || {}))
   143	
   144	    // Only null out missing relationships if a relationships object was provided
   145	    if (hasRelationshipsObject) {
   146	      for (const [relName, relInfo] of Object.entries(allRelationships)) {
   147	        if (!providedRelationships.has(relName)) {
   148	          if (relInfo.type === 'belongsTo') {
   149	            belongsToUpdates[relInfo.fieldName] = null
   150	          } else if (relInfo.type === 'manyToMany') {
   151	            // Add to manyToManyRelationships with empty array
   152	            manyToManyRelationships.push({
   153	              relName,
   154	              relDef: relInfo.relDef,
   155	              relData: []  // Empty array means delete all
   156	            })
   157	          }
   158	        }
   159	      }
   160	    }
   161	
   162	    // Merge belongsTo updates with attributes
   163	    if (Object.keys(belongsToUpdates).length > 0) {
   164	      context.inputRecord.data.attributes = {
   165	        ...context.inputRecord.data.attributes,
   166	        ...belongsToUpdates
   167	      }
   168	    }
   169	
   170	    // Centralised checkPermissions function
   171	    await scope.checkPermissions({
   172	      method: 'put',
   173	      originalContext: context,
   174	    })
   175	
   176	    await runHooks('beforeDataCall')
   177	    await runHooks('beforeDataCallPut')
   178	
   179	    // Apply field setters after validation and before storage
   180	    if (context.inputRecord?.data?.attributes) {
   181	      context.inputRecord.data.attributes = await applyFieldSetters(
   182	        context.inputRecord.data.attributes,
   183	        context.schemaInfo,
   184	        context,
   185	        api,
   186	        helpers
   187	      )
   188	    }
   189	
   190	    // Pass the operation type to the helper
   191	    await helpers.dataPut({
   192	      scopeName,
   193	      context
   194	    })
   195	    await runHooks('afterDataCallPut')
   196	    await runHooks('afterDataCall')
   197	
   198	    // Process many-to-many relationships after main record update/creation
   199	    for (const { relName, relDef, relData } of manyToManyRelationships) {
   200	      if (relDef?.through && api.anyapi?.links?.syncMany) {
   201	        await api.anyapi.links.syncMany({
   202	          context,
   203	          scopeName,
   204	          relName,
   205	          relDef,
   206	          relData,
   207	          isUpdate: context.isUpdate,
   208	        })
   209	        continue
   210	      }
   211	
   212	      await validatePivotResource(scopes, relDef, relName)
   213	
   214	      if (context.isUpdate) {
   215	        await updateManyToManyRelationship(null, {
   216	          api,
   217	          context: {
   218	            resourceId: context.id,
   219	            relDef,
   220	            relData,
   221	            transaction: context.transaction
   222	          }
   223	        })
   224	      } else if (relData.length > 0) {
   225	        await createPivotRecords(api, context.id, relDef, relData, context.transaction)
   226	      }
   227	    }
   228	
   229	    const ret = await handleRecordReturnAfterWrite({
   230	      context,
   231	      scopeName,
   232	      api,
   233	      scopes,
   234	      schemaStructure,
   235	      schemaRelationships,
   236	      scopeOptions,
   237	      vars,
   238	      runHooks,
   239	      helpers,
   240	      log
   241	    })
   242	
   243	    // Commit transaction if we created it
   244	    if (context.shouldCommit) {
   245	      await context.transaction.commit()
   246	      await runHooks('afterCommit')
   247	    }
   248	
   249	    return ret
   250	  } catch (error) {
   251	    await handleWriteMethodError(error, context, 'PUT', scopeName, log, runHooks)
   252	  }
   253	}
