-
Notifications
You must be signed in to change notification settings - Fork 22
feat(integrations.zendesk): add Zendesk ticket integration #319
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,8 @@ | ||
| import { Config, Integration } from '@o2s/integrations.mocked/integration'; | ||
| import { Config as ZendeskConfig, Integration as ZendeskIntegration } from '@o2s/integrations.zendesk/integration'; | ||
|
|
||
| import { ApiConfig } from '@o2s/framework/modules'; | ||
|
|
||
| export const TicketsIntegrationConfig: ApiConfig['integrations']['tickets'] = Config.tickets!; | ||
|
|
||
| export import Service = Integration.Tickets.Service; | ||
| export import Request = Integration.Tickets.Request; | ||
| export import Model = Integration.Tickets.Model; | ||
| export const TicketsIntegrationConfig: ApiConfig['integrations']['tickets'] = ZendeskConfig.tickets!; | ||
| export import Service = ZendeskIntegration.Tickets.Service; | ||
| export import Request = ZendeskIntegration.Tickets.Request; | ||
| export import Model = ZendeskIntegration.Tickets.Model; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
|
|
||
| # compiled output | ||
| /dist | ||
| /node_modules | ||
| /build | ||
|
|
||
| # Generated OpenAPI files | ||
| /scripts/oas/oas.yaml | ||
| /generated/zendesk/ | ||
|
|
||
| # Logs | ||
| logs | ||
| *.log | ||
| npm-debug.log* | ||
| pnpm-debug.log* | ||
| yarn-debug.log* | ||
| yarn-error.log* | ||
| lerna-debug.log* | ||
|
|
||
| # OS | ||
| .DS_Store | ||
|
|
||
| # Tests | ||
| /coverage | ||
| /.nyc_output | ||
|
|
||
| # IDEs and editors | ||
| /.idea | ||
| .project | ||
| .classpath | ||
| .c9/ | ||
| *.launch | ||
| .settings/ | ||
| *.sublime-workspace | ||
|
|
||
| # IDE - VSCode | ||
| .vscode/* | ||
| !.vscode/settings.json | ||
| !.vscode/tasks.json | ||
| !.vscode/launch.json | ||
| !.vscode/extensions.json | ||
|
|
||
| # dotenv environment variable files | ||
| .env | ||
| .env.development.local | ||
| .env.test.local | ||
| .env.production.local | ||
|
|
||
| # temp directory | ||
| .temp | ||
| .tmp | ||
|
|
||
| # Runtime data | ||
| pids | ||
| *.pid | ||
| *.seed | ||
| *.pid.lock | ||
|
|
||
| # Diagnostic reports (https://nodejs.org/api/report.html) | ||
| report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import apiConfig from '@o2s/prettier-config/api.mjs'; | ||
|
|
||
| /** | ||
| * @see https://prettier.io/docs/en/configuration.html | ||
| * @type {import("prettier").Config} | ||
| */ | ||
| const config = { | ||
| ...apiConfig, | ||
| }; | ||
|
|
||
| export default config; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| import { config } from '@o2s/eslint-config/api'; | ||
|
|
||
| /** @type {import("eslint").Linter.Config} */ | ||
| export default config; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| export default { | ||
| '*.{js,jsx,ts,tsx,css,scss}': ['prettier --write'], | ||
| '*.{js,jsx,ts,tsx}': () => 'tsc --noEmit', | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| { | ||
| "name": "@o2s/integrations.zendesk", | ||
| "version": "1.0.0", | ||
| "private": false, | ||
| "license": "MIT", | ||
| "exports": { | ||
| "./integration": "./dist/integration.js" | ||
| }, | ||
| "files": [ | ||
| "dist" | ||
| ], | ||
| "scripts": { | ||
| "prepare": "npm run fetch-oas && npm run generate-types", | ||
| "dev": "tsc && (concurrently \"tsc -w\" \"tsc-alias -w\")", | ||
| "build": "tsc && tsc-alias", | ||
| "lint": "tsc --noEmit && eslint . --max-warnings 0", | ||
| "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,css,scss,json}\"", | ||
| "fetch-oas": "tsx scripts/oas/fetch-zendesk-oas.ts", | ||
| "generate-types": "tsx scripts/oas/generate-zendesk-types.ts" | ||
| }, | ||
| "dependencies": { | ||
| "@nestjs/axios": "^3.0.0", | ||
| "@nestjs/common": "^10.0.0", | ||
| "@o2s/framework": "*", | ||
| "@o2s/utils.logger": "*", | ||
| "axios": "^1.13.2", | ||
| "rxjs": "^7.8.1" | ||
| }, | ||
| "devDependencies": { | ||
| "@hey-api/openapi-ts": "^0.87.4", | ||
| "@o2s/eslint-config": "*", | ||
| "@o2s/prettier-config": "*", | ||
| "@o2s/typescript-config": "*", | ||
| "concurrently": "^9.2.1", | ||
| "eslint": "^9.37.0", | ||
| "prettier": "^3.6.2", | ||
| "tsc-alias": "^1.8.10", | ||
| "tsx": "^4.19.2", | ||
| "typescript": "^5.7.3" | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import fs from 'node:fs'; | ||
| import path from 'node:path'; | ||
|
|
||
| async function fetchZendeskOAS() { | ||
| const oasUrl = 'https://developer.zendesk.com/zendesk/oas.yaml'; | ||
| const oasPath = path.resolve(__dirname, '../oas/oas.yaml'); | ||
|
|
||
| try { | ||
| const response = await fetch(oasUrl); | ||
| if (!response.ok) { | ||
| throw new Error(`Failed to fetch OAS: ${response.statusText}`); | ||
| } | ||
| const oasContent = await response.text(); | ||
| fs.mkdirSync(path.dirname(oasPath), { recursive: true }); | ||
| fs.writeFileSync(oasPath, oasContent); | ||
| console.log('Zendesk OAS downloaded successfully to', oasPath); | ||
| } catch (error) { | ||
| console.error('Error fetching Zendesk OAS:', error); | ||
| process.exit(1); | ||
| } | ||
| } | ||
|
|
||
| fetchZendeskOAS(); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { createClient } from '@hey-api/openapi-ts'; | ||
| import { existsSync, mkdirSync } from 'node:fs'; | ||
| import { resolve } from 'node:path'; | ||
|
|
||
| async function generateZendeskTypes(): Promise<void> { | ||
| const oasPath = resolve(__dirname, '../oas/oas.yaml'); | ||
| if (!existsSync(oasPath)) { | ||
| throw new Error(`OAS file not found at ${oasPath}. Run 'npm run fetch-oas' first.`); | ||
| } | ||
| const outputDirPath = resolve(__dirname, '../../generated/zendesk'); | ||
|
|
||
| try { | ||
| mkdirSync(outputDirPath, { recursive: true }); | ||
| await createClient({ | ||
| input: oasPath, | ||
| output: outputDirPath, | ||
| }); | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| console.log('Zendesk types generated successfully to', outputDirPath); | ||
| } catch (error) { | ||
| console.error('Error generating Zendesk types:', error); | ||
| process.exit(1); | ||
| } | ||
| } | ||
|
|
||
| generateZendeskTypes(); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import { HttpModule } from '@nestjs/axios'; | ||
|
|
||
| import { ApiConfig, Users } from '@o2s/framework/modules'; | ||
|
|
||
| import { Service as TicketsService } from './modules/tickets'; | ||
|
|
||
| export * as Integration from './modules/index'; | ||
|
|
||
| export const Config: Partial<ApiConfig['integrations']> = { | ||
| tickets: { | ||
| name: 'zendesk', | ||
| service: TicketsService, | ||
| imports: [HttpModule, Users.Module], | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * as Tickets from './tickets'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import { Tickets } from '@o2s/framework/modules'; | ||
|
|
||
| export * from './zendesk-ticket.service'; | ||
| export { ZendeskTicketService as Service } from './zendesk-ticket.service'; | ||
| export { ZendeskTicketModule as Module } from './zendesk-ticket.module'; | ||
|
|
||
| export import Request = Tickets.Request; | ||
| export import Model = Tickets.Model; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| import { Tickets } from '@o2s/framework/modules'; | ||
|
|
||
| import { type TicketObject } from '@/generated/zendesk'; | ||
|
|
||
| type ZendeskTicket = TicketObject; | ||
|
|
||
| export interface ZendeskComment { | ||
| id?: number; | ||
| author_id?: number; | ||
| body?: string; | ||
| created_at?: string; | ||
| public?: boolean; | ||
| attachments?: Array<{ | ||
| id?: number; | ||
| file_name?: string; | ||
| content_url?: string; | ||
| content_type?: string; | ||
| size?: number; | ||
| }>; | ||
| } | ||
|
|
||
| export function mapTicketToModel(ticket: ZendeskTicket, comments: ZendeskComment[] = []): Tickets.Model.Ticket { | ||
| let status: Tickets.Model.TicketStatus = 'OPEN'; | ||
| switch (ticket.status) { | ||
| case 'closed': | ||
| case 'solved': | ||
| status = 'CLOSED'; | ||
| break; | ||
| case 'pending': | ||
| case 'hold': | ||
| status = 'IN_PROGRESS'; | ||
| break; | ||
| default: | ||
| status = 'OPEN'; | ||
| } | ||
|
|
||
| let topic = 'GENERAL'; | ||
| const properties: Tickets.Model.TicketProperty[] = [ | ||
| { id: 'subject', value: ticket.subject || '' }, | ||
| { id: 'description', value: ticket.description || '' }, | ||
| ]; | ||
|
|
||
| if (ticket.custom_fields) { | ||
| ticket.custom_fields.forEach((field) => { | ||
| if (field.value !== null && field.value !== undefined) { | ||
| if (field.id === 27720325153053) { | ||
| topic = String(field.value).toUpperCase(); | ||
| } else { | ||
| properties.push({ | ||
| id: `custom_field_${field.id}`, | ||
| value: String(field.value), | ||
| }); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| const mappedComments = comments.map((comment) => ({ | ||
| author: { | ||
| name: `User ${comment.author_id}`, | ||
| email: '', | ||
| }, | ||
| date: comment.created_at || '', | ||
| content: comment.body || '', | ||
| })); | ||
|
Comment on lines
+58
to
+65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing actual author details for comments. Comment authors are displayed as placeholder text Consider either:
Example enhancement for bulk fetching: // In the service, before calling mapTicketToModel:
const authorIds = [...new Set(comments.map(c => c.author_id).filter(Boolean))];
const authors = await Promise.all(authorIds.map(id => fetchUser(id)));
const authorMap = new Map(authors.map(a => [a.id, a]));
// Then pass authorMap to mapper and use it:
const mappedComments = comments.map((comment) => {
const author = authorMap.get(comment.author_id);
return {
author: {
name: author?.name || `User ${comment.author_id}`,
email: author?.email || '',
},
// ...
};
});🤖 Prompt for AI Agents |
||
|
|
||
| const attachments: Tickets.Model.TicketAttachment[] = []; | ||
| comments.forEach((comment) => { | ||
| if (comment.attachments && comment.attachments.length > 0) { | ||
| comment.attachments.forEach((attachment) => { | ||
| attachments.push({ | ||
| name: attachment.file_name || '', | ||
| url: attachment.content_url || '', | ||
| size: attachment.size || 0, | ||
| author: { | ||
| name: `User ${comment.author_id}`, | ||
| email: '', | ||
| }, | ||
| date: comment.created_at || '', | ||
| ariaLabel: `Download ${attachment.file_name || 'attachment'}`, | ||
| }); | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| return { | ||
| id: ticket.id?.toString() || '', | ||
| createdAt: ticket.created_at || '', | ||
| updatedAt: ticket.updated_at || '', | ||
| topic, | ||
| type: (ticket.priority || 'NORMAL').toUpperCase(), | ||
| status, | ||
| properties, | ||
| comments: mappedComments.length > 0 ? mappedComments : undefined, | ||
| attachments: attachments.length > 0 ? attachments : undefined, | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import { HttpModule } from '@nestjs/axios'; | ||
| import { Module } from '@nestjs/common'; | ||
|
|
||
| import { Users } from '@o2s/framework/modules'; | ||
|
|
||
| import { ZendeskTicketService } from './zendesk-ticket.service'; | ||
|
|
||
| @Module({ | ||
| imports: [HttpModule, Users.Module], | ||
| providers: [ZendeskTicketService], | ||
| exports: [ZendeskTicketService], | ||
| }) | ||
| export class ZendeskTicketModule {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
currently you have to remember to run getch/generate scripts manually, let's add prepare script that takes care of that automatically