Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/api-harmonization/.env.local
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,6 @@ MEDUSAJS_PUBLISHABLE_API_KEY=
MEDUSAJS_ADMIN_API_KEY=

SEARCH_ARTICLES_INDEX_NAME=mock

ZENDESK_API_URL=
ZENDESK_API_TOKEN=
2 changes: 1 addition & 1 deletion apps/api-harmonization/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export const AuthModuleBaseModule = AuthModule.Module.register(AppConfig);

@Module({
imports: [
HttpModule,
HttpModule.register({ global: true }),
LoggerModule,
ConfigModule.forRoot({
isGlobal: true,
Expand Down
3 changes: 2 additions & 1 deletion packages/configs/integrations/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"dependencies": {
"@o2s/framework": "*",
"@o2s/integrations.mocked": "*",
"@o2s/integrations.strapi-cms": "*"
"@o2s/integrations.strapi-cms": "*",
"@o2s/integrations.zendesk": "*"
},
"devDependencies": {
"@o2s/eslint-config": "*",
Expand Down
11 changes: 5 additions & 6 deletions packages/configs/integrations/src/models/tickets.ts
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;
60 changes: 60 additions & 0 deletions packages/integrations/zendesk/.gitignore
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
11 changes: 11 additions & 0 deletions packages/integrations/zendesk/.prettierrc.mjs
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;
4 changes: 4 additions & 0 deletions packages/integrations/zendesk/eslint.config.mjs
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;
4 changes: 4 additions & 0 deletions packages/integrations/zendesk/lint-staged.config.mjs
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',
};
41 changes: 41 additions & 0 deletions packages/integrations/zendesk/package.json
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",
Comment on lines +14 to +16
Copy link
Collaborator

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

"prepare": "npm run fetch-oas && npm run generate-types" 

"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"
}
}
23 changes: 23 additions & 0 deletions packages/integrations/zendesk/scripts/oas/fetch-zendesk-oas.ts
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,
});
console.log('Zendesk types generated successfully to', outputDirPath);
} catch (error) {
console.error('Error generating Zendesk types:', error);
process.exit(1);
}
}

generateZendeskTypes();
15 changes: 15 additions & 0 deletions packages/integrations/zendesk/src/integration.ts
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],
},
};
1 change: 1 addition & 0 deletions packages/integrations/zendesk/src/modules/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * as Tickets from './tickets';
8 changes: 8 additions & 0 deletions packages/integrations/zendesk/src/modules/tickets/index.ts
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing actual author details for comments.

Comment authors are displayed as placeholder text User ${author_id} with empty emails, which degrades the user experience. While the service has a fetchUser method available, it's not used to populate actual author names.

Consider either:

  1. Fetching and caching user details in bulk before mapping comments, or
  2. Documenting this limitation if it's an intentional performance trade-off

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
In packages/integrations/zendesk/src/modules/tickets/zendesk-ticket.mapper.ts
around lines 58 to 65, the mapper currently uses placeholder author names and
empty emails for comment authors; replace this by collecting unique
comment.author_id values, bulk-fetching those users via the service's fetchUser
(or a bulk fetch helper), building an authorMap keyed by id, and then using that
map in the comments mapping to set author.name and author.email with fallbacks
to `User ${id}` and empty string; ensure the mapper signature or caller is
updated to accept the authorMap (or fetch/cache users before calling the
mapper), and add light caching or memoization to avoid duplicate remote calls
or, if this is an intentional trade-off, add clear documentation explaining the
limitation.


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 {}
Loading
Loading