Add file to Notes via REST API using NodeJS

I can successfully create the note.

I now need to upload the file and then link to the note.

Which I think I can do with this endpoint:

"/rest/v11_20/Notes/5f95bbe8-cd49-11ee-b015-fa163ec72422/file/filename"

I'm POSTing a file using FormData from my app and handling this on my NodeJS server.

Here I use axios to call the Endpoint above.

However, whatever format I pass the file in, I get either a 400 or a 422 returned from the Sugar API.

I've tried several things:

  • setting. removing / changing content-type 
  • setting / removing content disposition
  •  pass file as raw File, FormData, Buffer

// this adds in basepath and sets api version

const fileUrl = getFileUrl(config, noteId)

const fileResponse = await axios.post(fileUrl, file, {
   headers: {
      'OAuth-Token': accessToken,
   },
});

 
Has anybody had success doing this? Can you share your code?
Thanks
  • When I do this in php, I do the following:

            //////////////////////////////////////////////////////////   			
    		//Create note record - POST /<module>/:record
            //////////////////////////////////////////////////////////   			
    
    		$url = $base_url . "/Notes";
    		
    		$note_arguments = array(
    			"set_created_by" => true,
                "portal_flag" => false,
                "embed_flag" => false,
                "following" => false,
                "my_favorite" => false,
    		);
    		
    		$note_response = call($url, $oauth2_token_response->access_token, 'POST', $note_arguments);
    
            $note_id = $note_response->id;
    		
    		if ($note_id != "") {
    
    			//////////////////////////////////////////////////////////   			
    			//Upload note file - POST /Notes/<id>/file/filename
    			//////////////////////////////////////////////////////////   			
    			
    			$url = $base_url . "/Notes/".$note_id."/file/filename";
    			
    			if ((version_compare(PHP_VERSION, '5.5') >= 0)) {
    				$filedata = new CURLFile(realpath($dir.$data[0]),"",$data[2]);
    			} else {
    				$filedata = '@'.realpath($dir.$data[0]);
    			}
    			$file_arguments = array(
    				"format" => "sugar-html-json",
    				"delete_if_fails" => true,
    				"oauth_token" => $oauth2_token_response->access_token,
    				'filename' => $filedata,
    			);
    			$file_response = call($url, $oauth2_token_response->access_token, 'POST', $file_arguments, false,false,true);
    		}
    

    The call function (grown over years):

    /**
     * Generic function to make cURL request.
     * @param $url - The URL route to use.
     * @param string $oauthtoken - The oauth token.
     * @param string $type - GET, POST, PUT, DELETE. Defaults to GET.
     * @param array $arguments - Endpoint arguments.
     * @param array $encodeData - Whether or not to JSON encode the data.
     * @param array $returnHeaders - Whether or not to return the headers.
     * @param array $filenHeader - Whether or not to upload a file
     * @return mixed
     */
    function call(
        $url,
        $oauthtoken='',
        $type='GET',
        $arguments=array(),
        $encodeData=true,
        $returnHeaders=false,
    	$fileHeader=false
    )
    {
        $type = strtoupper($type);
    
        if ($type == 'GET')
        {
            $url .= "?" . http_build_query($arguments);
        }
    
        $curl_request = curl_init($url);
    
        if ($type == 'POST')
        {
            curl_setopt($curl_request, CURLOPT_POST, 1);
        }
        elseif ($type == 'PUT')
        {
            curl_setopt($curl_request, CURLOPT_CUSTOMREQUEST, "PUT");
        }
        elseif ($type == 'DELETE')
        {
            curl_setopt($curl_request, CURLOPT_CUSTOMREQUEST, "DELETE");
        }
    
        curl_setopt($curl_request, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
        curl_setopt($curl_request, CURLOPT_HEADER, $returnHeaders);
        curl_setopt($curl_request, CURLOPT_SSL_VERIFYHOST, 0);  // wichtig
        curl_setopt($curl_request, CURLOPT_SSL_VERIFYPEER, 0);  // wichtig
        curl_setopt($curl_request, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($curl_request, CURLOPT_FOLLOWLOCATION, 0);
    
        if (!empty($oauthtoken)) 
        {
    		if ($fileHeader) {
    			curl_setopt($curl_request, CURLOPT_HTTPHEADER, array(
    				"oauth-token: {$oauthtoken}"));
    		} else {
                curl_setopt($curl_request, CURLOPT_HTTPHEADER, array(
    				"oauth-token: {$oauthtoken}",
    				"Content-Type: application/json"));
    		}		
        }
        else
        {
            curl_setopt($curl_request, CURLOPT_HTTPHEADER, array(
    			"Content-Type: application/json"));
        }
    
        if (!empty($arguments) && $type !== 'GET')
        {
            if ($encodeData)
            {
                //encode the arguments as JSON
                $arguments = json_encode($arguments);
            }
            curl_setopt($curl_request, CURLOPT_POSTFIELDS, $arguments);
        }
    
        $result = curl_exec($curl_request);
    	
        if ($returnHeaders)
        {
            //set headers from response
            list($headers, $content) = explode("\r\n\r\n", $result ,2);
            foreach (explode("\r\n",$headers) as $header)
            {
                header($header);
            }
    
            //return the nonheader data
            return trim($content);
        }
    
        curl_close($curl_request);
    
        //decode the response from JSON
        $response = json_decode($result);
    
        return $response;
    }

  • Thanks for the reply  , I got there in the end.

    In order to send both the file and filename (and the note content) I used FormData.

    However, this would appear to require the use of a busboy (or similar) to handle the multi-part form data.

    Here is the relevant code, in case it helps someone else

    Angular Client Code to send note and file

    // Angular Service method to POST the file to the NodeJs Server
      addNote(jobId: string, payload: any) {
        const httpOptions = new CustomHttpParams(true);
    
        // it's necessary to use a form data object to get the boundary set correctly
        const fd = new FormData();
        fd.append('note', payload.file, payload.file.name);
        fd.append('filename', payload.filename);
        fd.append('note', JSON.stringify(payload.note));
    
        return this.http
          .post(`/functions/app/sugar/jobs/${jobId}/notes`, fd, {
            responseType: 'text',
            params: httpOptions,
          })
          .pipe(
            catchError((err) => this.handleError(err, [])),
            tap((response) => {
              console.log(response);
            })
          );
      }

    NodeJS server code to handle POST from Angular

    //NodeJS Handler
    const Busboy = require('busboy');
    const path = require('path');
    const os = require('os');
    const fs = require('fs');
    
    const addNoteEndpoint = async (request: Request, response: Response) => {
    
      const jobId = request.params.jobId;
    
      const uploads: any[] = [];
      const busboy = new Busboy({ headers: request.headers });
    
      // This callback will be invoked for each file uploaded
      busboy.on('file', (fieldName: string, file: any, filename: string, encoding: any, mimetype: any) => {
        // save the file locally (tmp memory)
        const filepath = path.join(os.tmpdir(), filename);
        uploads.push({ file: filepath, filename: filename });
        file.pipe(fs.createWriteStream(filepath));
      });
    
      let note: any, filename: string;
    
      busboy.on('field', (fieldName: string, val: any) => {
        if (fieldName === 'note') {
          note = val;
        } else if (fieldName === 'filename') {
          filename = val;
        } else {
          logger.info(`unknown field ${fieldName} => ${val}`);
        }
      });
    
      busboy.on('finish', async () => {
        if (uploads.length === 0) {
          response.end('no files found');
        }
    
        let upload = uploads[0];
        let file = upload.file;
    
        const uid = await addNote(jobId, note, filename, file);
    
        response.status(200).send(uid);
      });
    
      // The raw bytes of the upload will be in request.rawBody.
      // This line required to send it to busboy, and trigger file events
      //@ts-ignore
      busboy.end(request.rawBody);
    };

    Method to add file to Note using Sugar REST API

    //NodeJS method to call Sugar API and add file to Note (created prior)
    function getAddFileToNoteUrl(config: SugarConfig, noteId: string) {
      return `https://${config.basePath}/rest/v${config.apiVersion}/Notes/${noteId}/file/filename`;
    }
    
    async function addFileToNote({ noteId, file, filename }: Params): Promise<any> {
      const accessToken = await getAccessToken();
    
      const url = getAddFileToNoteUrl(config, noteId);
    
      const form = new FormData();
    
      form.append('filename', fs.createReadStream(file), filename);
    
      const fileResponse = await axios.post(url, form, {
        headers: {
          ...form.getHeaders(),
          'OAuth-Token': accessToken,
          'Cache-Control': 'no-cache',
        },
      });
    
      return fileResponse?.data;
    }

    Method to link note to a job using Sugar REST API

    // NodeJS method to link the note to a job (both already exist)
    function getLinkNoteToJobUrl(config: SugarConfig, jobId: string, noteId: string) {
      return `https://${config.basePath}/rest/v${config.apiVersion}/JOB_Jobs/${jobId}/link/job_jobs_activities_1_notes/${noteId}`;
    }
    
    async function linkNoteToJob({ jobId, noteId }: Params): Promise<any> {
      const accessToken = await getAccessToken();
    
      const linkNoteUrl = getLinkNoteToJobUrl(config, jobId, noteId);
    
      const linkResponse = await axios.post(linkNoteUrl, null, {
        headers: {
          'OAuth-Token': accessToken,
          'Cache-Control': 'no-cache',
        },
      });
    
      return linkResponse.data;
    }