diff --git a/14-operations/.DS_Store b/14-operations/.DS_Store new file mode 100644 index 0000000..79f4da6 Binary files /dev/null and b/14-operations/.DS_Store differ diff --git a/14-operations/code/package-lock.json b/14-operations/code/package-lock.json index 3fc44dc..13fad45 100644 --- a/14-operations/code/package-lock.json +++ b/14-operations/code/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@devrev/typescript-sdk": "1.1.28", + "@devrev/typescript-sdk-internal": "1.2.266", "@slack/web-api": "7.3.1", "axios": "1.7.9", "protobufjs": "7.3.0" @@ -1755,6 +1756,27 @@ "yargs": "^17.6.2" } }, + "node_modules/@devrev/typescript-sdk-internal": { + "version": "1.2.266", + "resolved": "https://npm.pkg.github.com/download/@devrev/typescript-sdk-internal/1.2.266/aab5b2ebdb0bb1c04027ca92737aabe9064fde64", + "integrity": "sha512-RJs2AsXPrZicZS3FyntAUwlLMaW0FChghd19zi8kk/ZgE29LuRWeTqmA9uyjkXFi9W7hzIxO/dRBCIhofE1S2A==", + "license": "ISC", + "dependencies": { + "@types/yargs": "^17.0.22", + "axios": "^1.7.4", + "dotenv": "^16.0.3", + "form-data": "^4.0.1", + "lru-cache": "^10.0.0", + "protobufjs": "^7.3.0", + "yargs": "^17.6.2" + } + }, + "node_modules/@devrev/typescript-sdk-internal/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", @@ -10554,6 +10576,27 @@ "yargs": "^17.6.2" } }, + "@devrev/typescript-sdk-internal": { + "version": "1.2.266", + "resolved": "https://npm.pkg.github.com/download/@devrev/typescript-sdk-internal/1.2.266/aab5b2ebdb0bb1c04027ca92737aabe9064fde64", + "integrity": "sha512-RJs2AsXPrZicZS3FyntAUwlLMaW0FChghd19zi8kk/ZgE29LuRWeTqmA9uyjkXFi9W7hzIxO/dRBCIhofE1S2A==", + "requires": { + "@types/yargs": "^17.0.22", + "axios": "^1.7.4", + "dotenv": "^16.0.3", + "form-data": "^4.0.1", + "lru-cache": "^10.0.0", + "protobufjs": "^7.3.0", + "yargs": "^17.6.2" + }, + "dependencies": { + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + } + } + }, "@eslint-community/eslint-utils": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", diff --git a/14-operations/code/package.json b/14-operations/code/package.json index 6c63fab..04bf998 100644 --- a/14-operations/code/package.json +++ b/14-operations/code/package.json @@ -55,6 +55,7 @@ }, "dependencies": { "@devrev/typescript-sdk": "1.1.28", + "@devrev/typescript-sdk-internal": "1.2.266", "@slack/web-api": "7.3.1", "axios": "1.7.9", "protobufjs": "7.3.0" diff --git a/14-operations/code/src/functions/operation_handler/get_last_coding_activity.ts b/14-operations/code/src/functions/operation_handler/get_last_coding_activity.ts new file mode 100644 index 0000000..7dafcf7 --- /dev/null +++ b/14-operations/code/src/functions/operation_handler/get_last_coding_activity.ts @@ -0,0 +1,219 @@ +import { + OperationContext, + ExecuteOperationInput, + OperationOutput, + OutputValue, + FunctionInput, + OperationBase, +} from '@devrev/typescript-sdk/dist/snap-ins'; +import { + Api, + LinksListParams, + TimelineEntriesListParams, + LinkEndpointType, + ListMode, + TimelineChangeEvent, + TimelineEntryType, + WorkType, + WorksListParams, + Link, +} from '@devrev/typescript-sdk-internal/dist/auto-generated/internal/private-internal-devrev-sdk'; +import { client } from '@devrev/typescript-sdk-internal'; + +export interface GetInput { + object_id: string; +} + +export class GetLastCodingActivity extends OperationBase { + constructor(e: FunctionInput) { + super(e); + } + + async run( + context: OperationContext, + input: ExecuteOperationInput, + _resources: any, + ): Promise { + const inputData = input.data as GetInput; + + // Validate input + if (!inputData.object_id || inputData.object_id === '') { + return OperationOutput.fromJSON({ + error: { + message: 'No ID provided', + type: 'InvalidRequest', + }, + }); + } + console.log('Input object ID:', inputData.object_id); + console.log('Starting to get last coding activity for object ID:', inputData.object_id); + + // Initialize the private DevRev SDK + const devrevSDK = client.setupInternal({ + endpoint: context.devrev_endpoint, + token: context.secrets.access_token, + }); + + // Check if the object ID is that of an enhancement + let issueIDs: string[] = []; + if (inputData.object_id.toLowerCase().includes('enh')) { + console.log('Object ID is an enhancement'); + const issueRequest: WorksListParams = { + type: [WorkType.Issue], + applies_to_part: [inputData.object_id], + }; + while (true) { + console.log('Fetching issues for object:', inputData.object_id); + const issueResponse = await devrevSDK.worksList(issueRequest); + //console.log('Issue response:', issueResponse); + if (issueResponse.data?.works) { + for (const work of issueResponse.data.works) { + issueIDs.push(work.id); + } + } + if (!issueResponse.data?.next_cursor) { + break; + } + issueRequest.cursor = issueResponse.data.next_cursor; + } + } else { + console.log('Object ID is not an enhancement'); + issueIDs.push(inputData.object_id); + } + + let latestEpochTime = 0; + let codeChangeLinks: Link[] = []; + + for (const issueID of issueIDs) { + try { + // Step 1: Fetch linked code_change objects using links.list + const linksListRequest: LinksListParams = { + object: issueID, + object_types: [LinkEndpointType.CodeChange], + }; + + while (true) { + console.log('Fetching links for object:', issueID); + const linksResponse = await devrevSDK.linksList(linksListRequest); + console.log('Links response:', linksResponse); + const entries = linksResponse.data?.links || []; + codeChangeLinks = [...codeChangeLinks, ...entries]; + const next_cursor = linksResponse.data?.next_cursor; + if (next_cursor) { + linksListRequest.cursor = next_cursor; + } else { + break; + } + } + + + //For each code_change object, traverse the timeline to get the latest coding activity + for (const link of codeChangeLinks) { + try { + const timelineRequest: TimelineEntriesListParams = { + object: link.target.id, + mode: ListMode.Before, + }; + + while (true) { + const timelineResponse = + await devrevSDK.timelineEntriesList(timelineRequest); + console.log('Timeline response:', timelineResponse); + for (const entry of timelineResponse.data?.timeline_entries || + []) { + if (entry.type == TimelineEntryType.TimelineChangeEvent) { + const timelineChangeEvent = entry as TimelineChangeEvent; + // Add null checks for nested properties + const event = timelineChangeEvent.event; + + // Safely access the source_time_stamp + let sourceTimeStamp: string | undefined; + if (event) { + try { + // Use a safer approach to access the property by traversing the event object + const eventObj = event as unknown as Record; + + // Check if it's an annotated event with microflow_action + if (eventObj['annotated']?.['microflow_action']?.['event_metadata']) { + const metadata = eventObj['annotated']['microflow_action']['event_metadata']; + // Find the source_time_stamp in the metadata array + const timeStampEntry = metadata.find((item: any) => item.key === 'source_time_stamp'); + if (timeStampEntry) { + sourceTimeStamp = timeStampEntry.value; + } + } + + // Fallback to previous methods if needed + if (!sourceTimeStamp) { + sourceTimeStamp = eventObj['source_time_stamp'] || + eventObj['data']?.['source_time_stamp'] || + eventObj['metadata']?.['keys']?.['source_time_stamp']; + } + + console.log('Source time stamp:', sourceTimeStamp); + } catch (error) { + console.error('Error accessing source_time_stamp:', error); + } + } + + if (sourceTimeStamp) { + console.log('Source time stamp:', sourceTimeStamp); + const epochTime = new Date(sourceTimeStamp).getTime(); + if (epochTime && epochTime > latestEpochTime) { + latestEpochTime = epochTime; + } + } + } + } + + const next_cursor = timelineResponse.data?.next_cursor; + if (next_cursor) { + timelineRequest.cursor = next_cursor; + } else { + break; + } + } + } catch (error) { + console.error('Error fetching timeline entries:', error); + } + } + } catch (error) { + console.error('Error fetching links:', error); + } + } + if (codeChangeLinks.length === 0) { + console.log('No code_change objects linked to this object'); + return OperationOutput.fromJSON({ + summary: `No code_change objects linked to ${inputData.object_id}`, + output: { + values: [{ justification: `No code_change objects linked to this object ${inputData.object_id}` }], + } as OutputValue, + }); + } + + if (latestEpochTime === 0) { + console.log('No coding activity found for this object'); + return OperationOutput.fromJSON({ + summary: `No coding activity yet, linked to this object ${inputData.object_id}`, + output: { + values: [{ justification: `No coding activity yet, linked to this object ${inputData.object_id}` }], + } as OutputValue, + }); + } + + let latestSrcTime = new Date(latestEpochTime).toISOString(); + console.log('Latest coding activity time:', latestSrcTime); + return OperationOutput.fromJSON({ + summary: `Latest coding activity linked to ${inputData.object_id} happened at ${latestSrcTime}`, + output: { + values: [ + { + last_coding_activity: [latestSrcTime], + justification: `Latest coding activity linked to ${inputData.object_id} happened at ${latestSrcTime}`, + }, + + ], + } as OutputValue, + }); + } +} diff --git a/14-operations/code/src/functions/operation_handler/get_temperature.ts b/14-operations/code/src/functions/operation_handler/get_temperature.ts deleted file mode 100644 index 475798d..0000000 --- a/14-operations/code/src/functions/operation_handler/get_temperature.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { client, publicSDK } from '@devrev/typescript-sdk'; -import { - Error as OperationError, - Error_Type, - ExecuteOperationInput, - FunctionInput, - OperationBase, - OperationContext, - OperationOutput, - OutputValue, -} from '@devrev/typescript-sdk/dist/snap-ins'; - -interface GetTemperatureInput { - city: string; -} - -export class GetTemperature extends OperationBase { - constructor(e: FunctionInput) { - super(e); - } - - // This is optional and can be used to provide any extra context required. - override GetContext(): OperationContext { - let baseMetadata = super.GetContext(); - const temperatures: Record = { - 'New York': 72, - 'San Francisco': 65, - Seattle: 55, - 'Los Angeles': 80, - Chicago: 70, - Houston: 90, - }; - - return { - ...baseMetadata, - metadata: temperatures, - }; - } - - async run(_context: OperationContext, input: ExecuteOperationInput, _resources: any): Promise { - const input_data = input.data as GetTemperatureInput; - - const temperature = _context.metadata ? _context.metadata[input_data.city] : null; - - let err: OperationError | undefined = undefined; - if (!temperature) { - err = { - message: 'City not found', - type: Error_Type.InvalidRequest, - }; - } - const temp = { - error: err, - output: { - values: [{ "temperature": temperature }], - } as OutputValue, - } - return OperationOutput.fromJSON(temp); - } -} diff --git a/14-operations/code/src/functions/operation_handler/index.ts b/14-operations/code/src/functions/operation_handler/index.ts index 32ce626..90a0568 100644 --- a/14-operations/code/src/functions/operation_handler/index.ts +++ b/14-operations/code/src/functions/operation_handler/index.ts @@ -2,18 +2,14 @@ import { OperationFactory } from '../../operations'; import { ExecuteOperationInput,FunctionInput, OperationMap } from '@devrev/typescript-sdk/dist/snap-ins'; // Operations -import { GetTemperature } from './get_temperature'; -import { PostCommentOnTicket } from './post_comment_on_ticket'; -import { SendSlackMessage } from './send_slack_message'; +import { GetLastCodingActivity } from './get_last_coding_activity'; /** * Map of operations with the slug mentioned in the manifest. * The key is the slug of the operation mentioned in the manifest and value is the operation class. */ const operationMap: OperationMap = { - get_temperature: GetTemperature, - post_comment_on_ticket: PostCommentOnTicket, - send_slack_message: SendSlackMessage, + get_last_coding_activity: GetLastCodingActivity, }; export const run = async (events: FunctionInput[]) => { diff --git a/14-operations/code/src/functions/operation_handler/post_comment_on_ticket.ts b/14-operations/code/src/functions/operation_handler/post_comment_on_ticket.ts deleted file mode 100644 index e8ef4c2..0000000 --- a/14-operations/code/src/functions/operation_handler/post_comment_on_ticket.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { client } from '@devrev/typescript-sdk'; -import { TimelineEntriesCreateRequestType } from '@devrev/typescript-sdk/dist/auto-generated/beta/beta-devrev-sdk'; -import { - Error as OperationError, - Error_Type, - ExecuteOperationInput, - FunctionInput, - OperationBase, - OperationContext, - OperationOutput, - OutputValue, -} from '@devrev/typescript-sdk/dist/snap-ins'; - -interface PostCommentOnTicketInput { - id: string; - comment: string; -} - -export class PostCommentOnTicket extends OperationBase { - constructor(e: FunctionInput) { - super(e); - } - - async run(context: OperationContext, input: ExecuteOperationInput, _resources: any): Promise { - const input_data = input.data as PostCommentOnTicketInput; - const ticket_id = input_data.id; - const comment = input_data.comment; - - let err: OperationError | undefined = undefined; - if (!ticket_id) { - err = { - message: 'Ticket ID not found', - type: Error_Type.InvalidRequest, - }; - } - - const endpoint = context.devrev_endpoint; - const token = context.secrets.access_token; - - const devrevBetaClient = client.setupBeta({ - endpoint: endpoint, - token: token, - }); - let ticket; - try { - const ticketResponse = await devrevBetaClient.worksGet({ - id: ticket_id, - }); - console.log(JSON.stringify(ticketResponse.data)); - ticket = ticketResponse.data.work; - } catch (e: any) { - err = { - message: 'Error while fetching ticket details:' + e.message, - type: Error_Type.InvalidRequest, - }; - return OperationOutput.fromJSON({ - error: err, - }); - } - - try { - const timelineCommentResponse = await devrevBetaClient.timelineEntriesCreate({ - body: comment, - type: TimelineEntriesCreateRequestType.TimelineComment, - object: ticket.id, - }); - console.log(JSON.stringify(timelineCommentResponse.data)); - let commentID = timelineCommentResponse.data.timeline_entry.id; - return OperationOutput.fromJSON({ - error: err, - output: { - values: [{ comment_id: commentID }], - } as OutputValue, - }); - } catch (e: any) { - err = { - message: 'Error while posting comment:' + e.message, - type: Error_Type.InvalidRequest, - }; - return OperationOutput.fromJSON({ - error: err, - }); - } - } -} diff --git a/14-operations/code/src/functions/operation_handler/send_slack_message.ts b/14-operations/code/src/functions/operation_handler/send_slack_message.ts deleted file mode 100644 index 91a0514..0000000 --- a/14-operations/code/src/functions/operation_handler/send_slack_message.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { client } from '@devrev/typescript-sdk'; -import { TimelineEntriesCreateRequestType } from '@devrev/typescript-sdk/dist/auto-generated/beta/beta-devrev-sdk'; -import { - Error as OperationError, - Error_Type, - ExecuteOperationInput, - FunctionInput, - OperationBase, - OperationContext, - OperationOutput, - OutputValue, -} from '@devrev/typescript-sdk/dist/snap-ins'; - -import { WebClient } from '@slack/web-api'; - -interface SendSlackMessageInput { - channel: string; - message: string; -} - -export class SendSlackMessage extends OperationBase { - constructor(e: FunctionInput) { - super(e); - } - async run(context: OperationContext, input: ExecuteOperationInput, resources: any): Promise { - const input_data = input.data as SendSlackMessageInput; - const channel_id = input_data.channel; - const comment = input_data.message; - - let err: OperationError | undefined = undefined; - if (!channel_id) { - err = { - message: 'Channel ID not found', - type: Error_Type.InvalidRequest, - }; - } - - console.log("context:", context); - - const slack_token = resources.keyrings.slack_token.secret; - let slackClient; - try { - console.log('Creating slack client'); - slackClient = new WebClient(slack_token); - console.log('Slack client created'); - } catch (e: any) { - console.log('Error while creating slack client:', e.message); - err = { - message: 'Error while creating slack client:' + e.message, - type: Error_Type.InvalidRequest, - }; - return OperationOutput.fromJSON({ - error: err, - output: { - values: [], - } as OutputValue, - }); - } - console.log('Sending message to slack channel:', channel_id); - try { - const result = await slackClient.chat.postMessage({ - channel: channel_id, - text: comment, - }); - console.log('Message sent: ', result.ts); - return OperationOutput.fromJSON({ - error: err, - output: { - values: [{ message_id: result.ts }], - } as OutputValue, - }); - } catch (e: any) { - console.log('Error while sending message:', e.message); - err = { - message: 'Error while sending message:' + e.message, - type: Error_Type.InvalidRequest, - }; - return OperationOutput.fromJSON({ - error: err, - output: { - values: [], - } as OutputValue, - }); - } - } -} diff --git a/14-operations/manifest.yaml b/14-operations/manifest.yaml index 7f9ebba..92bfda5 100644 --- a/14-operations/manifest.yaml +++ b/14-operations/manifest.yaml @@ -1,98 +1,45 @@ version: "2" -name: "Operations" -description: "Pack of operations" +name: "Risk prediction" +description: "Risk prediction operations" service_account: - display_name: Operations Bot + display_name: Latest coding activity functions: - name: operation_handler description: function to handle operations operations: - - name: get_temperature - display_name: Get Temperature - description: Operation to get the temperature of a city - slug: get_temperature + - name: get_last_coding_activity + display_name: Last coding timestamp for an object + description: Node that retrieves the last coding activity of an issue or an enhancement + slug: get_last_coding_activity function: operation_handler type: action # Inputs to the operation. inputs: fields: - - name: city - field_type: enum - allowed_values: - - New York - - San Francisco - - Los Angeles - - Chicago - - Houston + - name: object_id + description: Object ID to retrieve last coding activity for. + field_type: id + id_type: + - enhancement + - issue is_required: true - default_value: "New York" ui: - display_name: City + display_name: Object ID + # Outputs of the operation. outputs: fields: - - name: temperature - field_type: double + - name: last_coding_activity + field_type: timestamp ui: - display_name: Temperature - - name: post_comment_on_ticket - display_name: Post Comment on Ticket - description: Operation to post a comment on ticket - slug: post_comment_on_ticket - function: operation_handler - type: action - inputs: - fields: - - name: id - description: Ticket ID to post comment on. + display_name: Last coding activity + - name: justification + description: Justification for the operation. field_type: text is_required: true ui: - display_name: Ticket ID - - name: comment - description: Comment to post on ticket. - field_type: text - is_required: true - ui: - display_name: Comment - outputs: - fields: - - name: comment_id - field_type: text - ui: - display_name: Comment ID - - name: send_slack_message - display_name: Send Slack Message - description: Operation to send a message to a Slack channel/thread - slug: send_slack_message - function: operation_handler - type: action - keyrings: - - name: slack_token - display_name: Slack Connection - description: Connection to Slack - types: - - slack - inputs: - fields: - - name: channel - description: Channel to send message to. - field_type: text - is_required: true - ui: - display_name: Channel - - name: message - description: Message to send. - field_type: rich_text - is_required: true - ui: - display_name: Message - outputs: - fields: - - name: message_id - field_type: text - ui: - display_name: Message ID + display_name: Justification + placeholder: Justification for the operation