How to add worklog to Jira Issue using REST API (NodeJS)? - node.js

I am creating an add-on which should create worklogs automatically. I am using node-js (jira-connector).
I managed to get Issues, and to create new, but I am getting the error:
UnhandledPromiseRejectionWarning: Error: Missing 'worklog' property
when I want to add worklogs to a Issue
function updateIssueInJira()
{
var jira = getoAuth();
try {
return new Promise(() => {
jira.issue.addWorkLog({
opts: {
issueKey: 'NTS-4',
adjustEstimate: 'new',
newEstimade: '2d',
worklog: 'Testing'
}
})
});
} catch (error) {
console.log(error);
}
}
The definition of addWorkLog is:
/**
* Adds a new worklog entry to an issue.
*
* #method addWorkLog
* #memberOf IssueClient#
* #param {Object} opts The options to pass to the API. Note that this object must contain EITHER an issueId or
* issueKey property; issueId will be used over issueKey if both are present.
* #param {string} [opts.issueId] The id of the issue. EX: 10002
* #param {string} [opts.issueKey] The Key of the issue. EX: JWR-3
* #param {string} [opts.adjustEstimate] Allows you to provide specific instructions to update the remaining time
* estimate of the issue. Valid values are
* * "new" - sets the estimate to a specific value
* * "leave"- leaves the estimate as is
* * "manual" - specify a specific amount to increase remaining estimate by
* * "auto"- Default option. Will automatically adjust the value based on the
* new timeSpent specified on the worklog
* #param {string} [opts.newEstimate] (required when "new" is selected for adjustEstimate) the new value for the
* remaining estimate field. e.g. "2d"
* #param {string} [opts.reduceBy] (required when "manual" is selected for adjustEstimate) the amount to reduce the
* remaining estimate by e.g. "2d"
* #param {Object} opts.worklog See {#link: https://docs.atlassian.com/jira/REST/latest/#d2e1106}
* #param [callback] Called after the worklog is added.
* #return {P
romise} Resolved after the worklog is added.
*/
this.addWorkLog = function (opts, callback) {
if (!opts.worklog) {
throw new Error(errorStrings.NO_WORKLOG_ERROR);
}
var options = this.buildRequestOptions(opts, '/worklog', 'POST', opts.worklog, {
newEstimate: opts.newEstimate,
reduceBy: opts.reduceBy,
adjustEstimate: opts.adjustEstimate
});
return this.jiraClient.makeRequest(options, callback, 'Worklog Added');
};
Under getoAuth() I am doing my oAuth authentification, and I want to add the worklog to the Issue NTS-4.

I found a solution. I am sending the input directly via REST Call (POST). For that, I am using 'request'
function updateIssueInJira(oauth)
{
var testjson = {
author: {
emailAddress: "myemail"
},
comment: "Test",
timeSpentSeconds: "2700"
}
request.post({
url:'https://thelocation.com/rest/api/2/issue/ISSUEKEY/worklog/',
oauth:oauth,
json:true,
body: testjson
},
function (e, r, user) {
console.log(user)
});
}

Related

On an afterSubmit when we creating a copy of one inventory item (name with '-c') ,The original ID of item link should come in a field on a copy order

I tried this above, here I am getting a null value only from my previous record.
Kindly give some guidance to solve my questions.
thanks in advance.
/**
*#NApiVersion 2.0
*#NScriptType UserEventScript
*/
define(["N/url", "N/record", "N/runtime"], function (url, record, runtime) {
function afterSubmit(context){
var recordobj = context.newRecord;
var prevItemrecord= context.oldRecord;
var Itemname = recordobj.getValue({fieldId:'itemid'});
var prevItemname = prevItemrecord.getValue({fieldId : 'itemid'});
var Type=context.type;
var checkbox=recordobj.getValue({fieldId:'custitem17'});
if(Type== context.UserEventType.CREATE)
if((Itemname=prevItemname+'-c')&&(checkbox=true))
record.submitFields({
type: recordobj.type,
id: recordobj.id,
values:{custitem_item_link:prevItemname}
});
}
return{
afterSubmit:afterSubmit
}
});
This is my code
On create there is no old record.
Since you are trying to update the same record as was just created you are better off having this in a beforeSubmit event script
if((Itemname=prevItemname+'-c')&&(checkbox=true)) this is an error
if((Itemname == prevItemname+'-c') && checkbox) is more what you need
If you are trying to capture a copy operation you can set that up in the beforeLoad event that you use in the beforeSubmit event.
function beforeLoad(ctx){
if(ctx.type == ctx.UserEventType.COPY){
if(ctx.form){
ctx.form.addField({
id:'custpage_original_item',
label:'Copied Item',
type:ui.FieldType.SELECT,
source:'item'
}).updateDisplayType({
displayType:ui.FieldDisplayType.HIDDEN
}).defaultValue = ctx.request.parameters.id;
// your naming makes me wonder if you are trying to link to the
// source item rather than just saving a reference to the source
// item's name
/*
* Using the original item's name like below is closer to what you
* posted but I think by the time the script runs the itemid field
* has been cleared.
* ctx.form.addField({
* id:'custpage_original_item_name',
* label:'Copied Item Name',
* type:ui.FieldType.TEXT
* }).updateDisplayType({
* displayType:ui.FieldDisplayType.HIDDEN
* }).defaultValue = ctx.newRecord.getValue({fieldId:'itemid'});
*/
}
}
}
function beforeSubmit(ctx){
if(ctx.type == ctx.UserEventType.CREATE){
const itemRec = ctx.newRecord;
if(itemRec.getValue({fieldId:'custitem17'})){
const sourceId = itemRec.getValue({fieldId:'custpage_original_item'})
if(sourceId){
itemRec.setValue({
fieldId:'custitem_item_link:prevItemname',
value:sourceId
});
/* or use a search function to look up the original item's name
* and then test the new item's name.
*/
}
}
}
}

How to extend Tabulator with resizable column plugin

I thought this would be shown in the docs but I'm trying to get column resizing working with Tabulator.
This page just describes how to import the plugin:
import {ResizeTableModule} from 'tabulator-tables';
But what do you with this import? I thought you could add it to Tabulator.extendModule(ResizeTableModule) but that does nothing. I couldn't find any examples using plugins modularly like this. Thanks for the help.
To get column resizing working, you can import ResizeColumnsModule as follows:
import { Tabulator, ResizeColumnsModule } from "tabulator-tables";
Tabulator.registerModule(ResizeColumnsModule)
Example: https://codesandbox.io/s/gifted-wescoff-nw337l?file=/src/index.js
As per docs correct way to extend a module is like
/**
* #param {String} moduleName
* #param {any} configs
* #param {any} newconfig
*/
Tabulator.extendModule("resizeTable", "config", {
//Your New Config
name: "__int__",
});
I am afriad as ResizeTableModule module does not expose any other config but you can define them by extending the class
import { Tabulator, ResizeColumnsModule, ResizeTableModule } from "tabulator-tables";
class MyResizeTableModule extends ResizeTableModule {
constructor(table) {
super(table);
this.config = MyResizeTableModule.config;
}
}
MyResizeTableModule.config = {};
Tabulator.registerModule([MyResizeTableModule, ResizeColumnsModule]);
/**
* #param {String} moduleName
* #param {any} configs
* #param {any} newconfig
*/
Tabulator.extendModule("resizeTable", "config", {
//Your New Config
name: "__int__",
});
See Codesandbox

Delete all entries from a custom record from UE Script

I have a UE script that saves entry from a custom record into rejection reason on vendor bill. This part is working fine and this script is able to save the rejection reason to overcome the limitations of the Employee Centre users that do not have write access on vendor bill. However, since I am fairly new to scripting, I am not able to delete all the records from the custom record every time before the script executes. If someone could help me with the related code to delete all the existing records in the custom record, that would be great. I am adding my existing code for your reference.
/**
#NApiVersion 2.x
#NScriptType UserEventScript
#NModuleScope SameAccount
*/
define(['N/record'],
function(record) {
/**
* Function definition to be triggered before record is loaded.
*
* #param {Object} scriptContext
* #param {Record} scriptContext.newRecord - New record
* #param {string} scriptContext.type - Trigger type
* #param {Form} scriptContext.form - Current form
* #Since 2015.2
*/
function beforeLoad(scriptContext)
{
}
/**
* Function definition to be triggered before record is loaded.
*
* #param {Object} scriptContext
* #param {Record} scriptContext.newRecord - New record
* #param {Record} scriptContext.oldRecord - Old record
* #param {string} scriptContext.type - Trigger type
* #Since 2015.2
*/
function beforeSubmit(scriptContext)
{
}
/**
* Function definition to be triggered before record is loaded.
*
* #param {Object} scriptContext
* #param {Record} scriptContext.newRecord - New record
* #param {Record} scriptContext.oldRecord - Old record
* #param {string} scriptContext.type - Trigger type
* #Since 2015.2
*/
function afterSubmit(scriptContext)
{
// Get the value of the Reject reason
var rejReason = scriptContext.newRecord.getValue({
fieldId: 'custrecord165' // Change this according to the internal id of the Field Reject Reason in the Custom Record Created
});
// Get the ID of the Vendor Bill Reject reason
var venbillID = scriptContext.newRecord.getValue({
fieldId: 'custrecord166' // Change this according to the internal id of the Field Reject Reason in the Custom Record Created
});
// populate the Reject Reason in the Vendor Bill Rejection Reason field
var id = record.submitFields({
type: record.Type.VENDOR_BILL,
id: venbillID,
values: {
custbody_rt_vendbill_rej_reason: rejReason //'testMSG01'
//custrecord165: 'testMSG'//rejReason // Change custbody1 to the internal id of the custom field Reject Reason in the Vendor Bill Record
},
options: {
enableSourcing: false,
ignoreMandatoryFields : true
}
});
}
return {
beforeLoad: beforeLoad,
beforeSubmit: beforeSubmit,
afterSubmit: afterSubmit
};
});
I have implemented such a requirement before. As I understand from your question, you need to delete all the entries from the custom record every time the script executes. This part can be added in the beginning of your code. Below is the sample code to help you with deleting all data from the custom record. In below snippet you need to replace "customrecord_tracker" with your custom record internal id and make other id changes as per your requirement
Please let me know how this goes for you! Happy coding! :)
//Search on the custom record to get the all the results
var searchTracker = search.create({
type: 'customrecord_tracker',
filters: ['isinactive', search.Operator.IS,'F'],
columns:['internalid']
});
var searchIcResults = searchTracker.run();
log.debug('searchIcResults',searchIcResults);
//Get the range of 1000 search results, if your search results are going to be less than 999 then you need not change anything here
var jeSearch = searchIcResults.getRange({
start: 0,
end: 999
});
if (jeSearch && jeSearch.length > 0) {
for (var itr = 0; itr < jeSearch.length; itr++) {
var internalId = jeSearch[itr].getValue({
name: 'internalid'
});
var deleteCustRecord = record.delete({
type: 'customrecord_tracker',
id: internalId
});
}
}

"SuiteScript 2.0 entry point scripts must implement one script type function" Error

I'm trying to upload this code to NetSuite
/**
* #NApiVersion 2.0
* #NScriptType ClientScript
* #NModuleScope SameAccount
*/
define(['N/ui/dialog'],
function(dialog){
/**
* Validation function to be executed when sublist line is committed.
*
* #param {Object} context
* #param {Record} context.currentRecord - Current form record
* #param {string} context.sublistId - Sublist name
*
* #returns {boolean} Return true if sublist line is valid
*
* #since 2015.2
*/
function validadeRate(context){
try{
var currentRecord = context.currentRecord
var sublistName = context.sublistId
if(sublistname ==='expense'){
var categ = CurrentRecord.getCurrentSublistValue({
sublistId: sublistName,
fieldId: 'category'
})
if ((categ = 259) && (rate != 0.819)){
var currIndex = currentRecord.getCurrentSublistIndex({
sublistId: sublistName
})
currIndex +=1
var options = {
title : 'Rate Incorreto!',
message:'Por favor, verifique o valor informado no campo Rate na linha ' + currIndex + '.',
}
dialog.alert(options).then(function (result) { }).catch(function(result){})
return false
}
}
return true
}
catch(ex){
log.error('validateLine: ', ex.message)
}
}
return {
validadeRate : validadeRate
}
});
But I'm getting this error when I'm trying to upload to file to Netsuite:
Notice
SuiteScript 2.0 entry point scripts must implement one script type function.*
This is part of a function that will validade the rate for one expense category.
How can I solve this?
thanks in advance!
This is NetSuite's 'Entry Point Script Validation' saying that the script is invalid because it doesn't include one of the predefined entry point (event) functions. These functions are:
fieldChanged
lineInit
pageInit
postSourcing
saveRecord
sublistChanged
validateDelete
validateField
validateInsert
validateLine
You can work around this validation and upload the script by adding one of those entry points, even if it does nothing. For example, inside your function (dialog) function you can add a pageInit() function:
function pageInit(scriptContext) {}
and change your return block to:
return {
validadeRate : validadeRate,
pageInit: pageInit
}
Now it has a valid entry point and the validation should pass.
However, there may be an even easier way. It appears (going by the JSDoc block), that your validadeRate function is supposed to be triggered each time a sublist line is added. This is exactly what the validateLine entry point is for. So you could just change the key in your return block to "validateLine"
return {
validateLine: validadeRate
}
and NetSuite would know to call validadeRate each time a line is added.
You have specified this as a Client Script module, but have not assigned a handler to any of the Client Script entry points. Read the Help document SuiteScript 2.0 Client Script Entry Points and API, and implement any one of the entry points in your module.
Change return function as below. and test once.
return
{
validateLine : validadeRate
}

oauth_signature invalid error while using Magento Rest API with GET filters

I'm having a hard time fetching orders from Magento REST API when I use its GET filters like http://localhost/magento/api/rest/orders/?filter[1][attribute]=entity_id&filter[1][gt]=70&page=1&limit=100
It is giving a "error":[{"code":401,"message":"oauth_problem=signature_invalid"}]
When I try hitting the same API endpoint using a REST Client like Postman, I'm getting back the desired results JSON.
I suspect the square brackets in the filter query might be causing a problem in generating a Oauth signature. All the endpoints without GET filters are working fine.
I'm using the Request node module to make the GET request with the oauth headers.
Is there any fix to avoid the signaturre invalid error?
The problem was within the Request node module I was using to generate a OAuth signature.It didn't factor for square brackets in the URL. I modified the code in the module for including square brackets. Changing the OAuth signature generation method fixed it for me
Since it took me a little while to figure out how to do this I figured I would pass on what I learned. My goal was to make a single request to the Magento’s REST API to return orders with specific order statuses. Looking through the GET filters documentation page wasn’t really helpful until I saw the very last line. Here is the request that I came up with that worked:
http://magentohost.com/api/rest/orders?filter[1][attribute]=status&filter[1][in][1]=pending&filter[1][in][2]=processing
The above request will get you a list of all orders with a status of “pending” or “processing”.
Ref: http://magentoforce.com/2014/08/magento-rest-api-multiple-in-get-filters/
For anyone who is wanting the answer to this here is what worked for me:
const axios = require('axios');
const crypto = require('crypto');
exports.handler = async (event) => {
let base_uri = 'https://YOUR_URL.com'; (ANYTHING UP TO THE /PATH)
const consumer_key = '';
const token_key = '';
const consumer_secret = '';
const token_secret = '';
==> CHANGE THE QUERY PARAMS TO WHATEVER YOU HAVE <==
const queryParameters = {
'searchCriteria[filterGroups][0][filters][0][field]': 'sku',
'searchCriteria[filterGroups][0][filters][0][condition_type]': 'eq',
'searchCriteria[filterGroups][0][filters][0][value]': 'aaaaaaaaa',
};
const timestamp = Math.floor(Date.now() / 1000);
const nonce = await generateNonce();
==> ADD IN YOUR PATH, IT WILL BE APPENDED TO THE BASE URL <==
const path = 'products';
base_uri = `${base_uri}/${path}`;
const reqsign256 = hmacsign256('GET', base_uri, { ...queryParameters, oauth_consumer_key: consumer_key, oauth_nonce: nonce, oauth_signature_method: 'HMAC-SHA256', oauth_timestamp: timestamp, oauth_token: token_key, oauth_version: '1.0' }, consumer_secret, token_secret);
const config = {
method: 'get',
url: base_uri,
headers: {
Authorization: `OAuth oauth_consumer_key="${consumer_key}",oauth_token="${token_key}",oauth_signature_method="HMAC-SHA256",oauth_timestamp="${timestamp}",oauth_nonce="${nonce}",oauth_version="1.0",oauth_signature="${reqsign256}"`
},
params: queryParameters,
};
let response = {};
return await axios(config)
.then(async (response) => {
response = {
statusCode: 200,
body: JSON.stringify(response.data),
};
return response;
})
.catch((error) => {
response = {
statusCode: 200,
body: JSON.stringify(error.message),
};
return response;
});
};
/**
*
* #returns A random string of 11 characters
*/
function generateNonce() {
let nonce = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 11; i++) {
nonce += possible.charAt(Math.floor(Math.random() * possible.length));
}
return nonce;
}
/**
*
* #param key
* #param body
* #param algorithm
* #description This is generating the signature using the imported crypto package.
* #returns The generated signature;
*/
function sha(key, body, algorithm) {
return crypto.createHmac(algorithm, key).update(body).digest('base64');
}
/**
*
* #param str
* #description The rfc3986 function takes a string as input and returns its encoded representation as per the rules specified in RFC 3986
* for URI (Uniform Resource Identifier) component encoding. It does this by first using encodeURIComponent to encode the string,
* which replaces certain characters with their percent-encoded representations. It then replaces characters such as !, *, (, ), and '
* with their respective percent-encoded representations, which are specified in RFC 3986 as being reserved characters that must be percent-encoded.
* #returns returns the encoded str value
*/
function rfc3986(str) {
return encodeURIComponent(str).replace(/!/g, '%21').replace(/\*/g, '%2A').replace(/\(/g, '%28').replace(/\)/g, '%29').replace(/'/g, '%27');
}
/**
*
* #param obj
* #description The map function takes an object as input and returns an array of its key-value pairs.
* It does this by iterating through the properties of the object using a for...in loop, and for each property, it checks its value.
* If the value is an array, it pushes an array with the property key and each value of the array into the result array.
* If the value is an object, it pushes an array with a string representation of the property key concatenated with the property name in square brackets
* and the property value into the result array.
* If the value is neither an array nor an object, it pushes an array with the property key and value into the result array.
* Finally, it returns the result array.
* #returns arr
*/
function map(obj) {
let key,
val,
arr = [];
for (key in obj) {
val = obj[key];
if (Array.isArray(val)) for (let i = 0; i < val.length; i++) arr.push([key, val[i]]);
else if (typeof val === 'object') for (let prop in val) arr.push([key + '[' + prop + ']', val[prop]]);
else arr.push([key, val]);
}
return arr;
}
/**
*
* #param a
* #param b
* #description Used to sort the oauth paramters into ascending order -- this is required for oauth1 to work.
* #returns the comparison result
*/
function compare(a, b) {
return a > b ? 1 : a < b ? -1 : 0;
}
/**
*
* #param httpMethod
* #param base_uri
* #param params
* #description
* 1. First, the name and value of each parameter are encoded
* 2. The parameters are sorted by name, using ascending byte value ordering. If two or more parameters share the same name, they are sorted by their value.
* 3. The name of each parameter is concatenated to its corresponding value using an "=" character (ASCII code 61) as a separator, even if the value is empty.
* for example in this case it is assigning oauth_nonce = 'foo' or oauth_signature_method = 'HMAC-SHA256' etc
* 4. The sorted name/value pairs are concatenated together into single string by using an "&" character (ASCII code 38) as separator.
* The final output will be something like this:
* GET&https%3A%2F%2Fstaging2.ospreylondon.com%2Frest%2FV1%2Fproducts&oauth_consumer_key%3Dxxx%26oauth_nonce%3xxxoauth_signature_method%3DHMAC-SHA256%26oauth_timestamp%3Dxxx%26oauth_token%3xxx%26oauth_version%3D1.0%26 ==> There will also be any url paramaters added at the end.
* #returns base url
*/
function generateBase(httpMethod, base_uri, params) {
let normalized = map(params)
// step 1
.map(function (p) {
return [rfc3986(p[0]), rfc3986(p[1] || '')];
})
// step 2
.sort(function (a, b) {
return compare(a[0], b[0]) || compare(a[1], b[1]);
})
//step 3
.map(function (p) {
return p.join('=');
})
//step 4
.join('&');
let base = [rfc3986(httpMethod ? httpMethod.toUpperCase() : 'GET'), rfc3986(base_uri), rfc3986(normalized)].join('&');
return base;
}
/**
*
* #param httpMethod
* #param base_uri
* #param params
* #param consumer_secret
* #param token_secret
* #description This takes the paramaters passed in, creates a base uri, creates a key (consumer secret and token secret with & between them)
* #returns this then returns the result of the sha method ==> this is the signature used in the request.
*/
function hmacsign256(httpMethod, base_uri, params, consumer_secret, token_secret) {
let base = generateBase(httpMethod, base_uri, params);
let key = [consumer_secret || '', token_secret || ''].map(rfc3986).join('&');
return sha(key, base, 'sha256');
}
Here is the detailed answer...
http://magentoforce.com/2014/08/magento-rest-api-multiple-in-get-filters/

Resources