Skip to content

Commit 461d583

Browse files
vogelskampsjfrhafeLemonaOna
authored
implement task logic
* 🚧 implement /tasks routes * 🚧 WIP * ✨ add route for creating follow up task * add logging * add task.controller tests --------- Co-authored-by: hafemann <[email protected]> Co-authored-by: Leona Kuse <[email protected]>
1 parent 5dcdeda commit 461d583

File tree

7 files changed

+285
-0
lines changed

7 files changed

+285
-0
lines changed
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import { Response } from 'express';
2+
import { BridgeRequest, FollowUpWithIntegrationEntities } from '../models';
3+
import { TaskController } from './task.controller';
4+
5+
describe('Task Controller', () => {
6+
const mockAdapter = {
7+
getTasks: jest.fn(),
8+
findAllByQuery: jest.fn(),
9+
createFollowUp: jest.fn(),
10+
};
11+
const mockNext = jest.fn();
12+
13+
describe('findAllByQuery', () => {
14+
beforeEach(() => jest.clearAllMocks());
15+
16+
it('Should check for providerConfig', async () => {
17+
const controller = new TaskController(mockAdapter);
18+
19+
const result = await controller.findAllByQuery(
20+
{} as BridgeRequest<void>,
21+
{} as Response,
22+
mockNext,
23+
);
24+
25+
expect(result).toBeUndefined();
26+
expect(mockNext).toHaveBeenCalled();
27+
});
28+
29+
it('Should check if adapter.getTasks is implemented', async () => {
30+
const controller = new TaskController({});
31+
32+
const result = await controller.findAllByQuery(
33+
{} as BridgeRequest<void>,
34+
{} as Response,
35+
mockNext,
36+
);
37+
38+
expect(result).toBeUndefined();
39+
expect(mockNext).toHaveBeenCalled();
40+
});
41+
42+
it('Should handle erroneous adapter.getTasks call', async () => {
43+
const controller = new TaskController(mockAdapter);
44+
45+
mockAdapter.getTasks.mockRejectedValue(null);
46+
47+
const result = await controller.findAllByQuery(
48+
{
49+
providerConfig: {
50+
userId: '123',
51+
apiKey: '123123123',
52+
apiUrl: ':)',
53+
locale: 'de-DE',
54+
},
55+
} as BridgeRequest<void>,
56+
{} as Response,
57+
mockNext,
58+
);
59+
60+
expect(result).toBeUndefined();
61+
expect(mockNext).toHaveBeenCalled();
62+
});
63+
64+
it('Should resolve happy path', async () => {
65+
const controller = new TaskController(mockAdapter);
66+
const mockResponse = { json: jest.fn() };
67+
68+
mockAdapter.getTasks.mockResolvedValue([]);
69+
70+
const req = {
71+
providerConfig: {
72+
userId: '123',
73+
apiKey: '123123123',
74+
apiUrl: ':)',
75+
locale: 'de-DE',
76+
},
77+
} as BridgeRequest<void>;
78+
79+
const result = await controller.findAllByQuery(
80+
req,
81+
mockResponse as unknown as Response,
82+
mockNext,
83+
);
84+
85+
expect(result).toBeUndefined();
86+
expect(mockAdapter.getTasks).toHaveBeenCalledWith(
87+
req,
88+
req.providerConfig,
89+
);
90+
expect(mockResponse.json).toHaveBeenCalledWith([]);
91+
});
92+
});
93+
94+
describe('create', () => {
95+
it('Should check for providerConfig', async () => {
96+
const controller = new TaskController(mockAdapter);
97+
98+
const result = await controller.create(
99+
{} as BridgeRequest<FollowUpWithIntegrationEntities>,
100+
{} as Response,
101+
mockNext,
102+
);
103+
104+
expect(result).toBeUndefined();
105+
expect(mockNext).toHaveBeenCalled();
106+
});
107+
108+
it('Should check if adapter.createFollowUp is implemented', async () => {
109+
const controller = new TaskController({});
110+
111+
const result = await controller.create(
112+
{} as BridgeRequest<FollowUpWithIntegrationEntities>,
113+
{} as Response,
114+
mockNext,
115+
);
116+
117+
expect(result).toBeUndefined();
118+
expect(mockNext).toHaveBeenCalled();
119+
});
120+
121+
it('Should handle erroneous adapter.createFollowUp call', async () => {
122+
const controller = new TaskController(mockAdapter);
123+
124+
mockAdapter.createFollowUp.mockRejectedValue(null);
125+
126+
const result = await controller.create(
127+
{
128+
providerConfig: {
129+
userId: '123',
130+
apiKey: '123123123',
131+
apiUrl: ':)',
132+
locale: 'de-DE',
133+
},
134+
} as BridgeRequest<FollowUpWithIntegrationEntities>,
135+
{} as Response,
136+
mockNext,
137+
);
138+
139+
expect(result).toBeUndefined();
140+
expect(mockNext).toHaveBeenCalled();
141+
});
142+
143+
it('Should resolve happy path', async () => {
144+
const controller = new TaskController(mockAdapter);
145+
const mockResponse = { json: jest.fn() };
146+
147+
const followUpId = 123;
148+
149+
mockAdapter.createFollowUp.mockResolvedValue(followUpId);
150+
151+
const req = {
152+
providerConfig: {
153+
userId: '123',
154+
apiKey: '123123123',
155+
apiUrl: ':)',
156+
locale: 'de-DE',
157+
},
158+
} as BridgeRequest<FollowUpWithIntegrationEntities>;
159+
160+
const result = await controller.create(
161+
req,
162+
mockResponse as unknown as Response,
163+
mockNext,
164+
);
165+
166+
expect(result).toBeUndefined();
167+
expect(mockAdapter.createFollowUp).toHaveBeenCalledWith(
168+
req.providerConfig,
169+
undefined,
170+
);
171+
expect(mockResponse.json).toHaveBeenCalledWith({ followUpId });
172+
});
173+
});
174+
});

src/controllers/task.controller.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { NextFunction, Response } from 'express';
2+
import {
3+
Adapter,
4+
BridgeRequest,
5+
FollowUpWithIntegrationEntities,
6+
} from '../models';
7+
import { infoLogger } from '../util';
8+
9+
export class TaskController {
10+
constructor(private readonly adapter: Adapter) {}
11+
12+
async findAllByQuery(
13+
req: BridgeRequest<void>,
14+
res: Response,
15+
next: NextFunction,
16+
) {
17+
const { providerConfig } = req;
18+
19+
if (!providerConfig) {
20+
next(new Error('Provider config not found'));
21+
return;
22+
}
23+
24+
if (!this.adapter.getTasks) {
25+
next(new Error('Method not implemented'));
26+
return;
27+
}
28+
29+
infoLogger('findAllByQuery', 'START', providerConfig.apiKey);
30+
31+
try {
32+
const followUps = await this.adapter.getTasks(req, providerConfig);
33+
34+
infoLogger(
35+
'findAllByQuery',
36+
`Received ${followUps.length} follow ups`,
37+
providerConfig.apiKey,
38+
);
39+
40+
infoLogger('findAllByQuery', 'END', providerConfig.apiKey);
41+
res.json(followUps);
42+
} catch (err) {
43+
next(err);
44+
}
45+
}
46+
47+
async create(
48+
req: BridgeRequest<FollowUpWithIntegrationEntities>,
49+
res: Response,
50+
next: NextFunction,
51+
) {
52+
const { providerConfig } = req;
53+
54+
if (!providerConfig) {
55+
next(new Error('Provider config not found'));
56+
return;
57+
}
58+
59+
if (!this.adapter.createFollowUp) {
60+
next(new Error('Method not implemented'));
61+
return;
62+
}
63+
64+
try {
65+
const followUpId = await this.adapter.createFollowUp(
66+
providerConfig,
67+
req.body,
68+
);
69+
res.json({ followUpId });
70+
} catch (err) {
71+
next(err);
72+
}
73+
}
74+
}

src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { CustomRouter } from './models/custom-router.model';
2525
import { CustomRoute } from './models/custom-routes.model';
2626
import { errorLogger, getTokenCache, infoLogger } from './util';
2727
import { getContactCache } from './util/get-contact-cache';
28+
import { TaskController } from './controllers/task.controller';
2829

2930
const PORT: number = Number(process.env.PORT) || 8080;
3031

@@ -53,6 +54,7 @@ export function start(
5354
tokenCache = getTokenCache();
5455

5556
const controller: Controller = new Controller(adapter, contactCache);
57+
const taskController: TaskController = new TaskController(adapter);
5658

5759
app.get('/contacts', (req, res, next) =>
5860
controller.getContacts(req, res, next),
@@ -150,6 +152,12 @@ export function start(
150152
controller.handleWebhook(req, res, next),
151153
);
152154

155+
app.get('/tasks', (req, res, next) =>
156+
taskController.findAllByQuery(req, res, next),
157+
);
158+
159+
app.post('/tasks', (req, res, next) => taskController.create(req, res, next));
160+
153161
app.use(errorHandlerMiddleware);
154162

155163
customRouters.forEach(({ path, router }) => app.use(path, router));

src/models/adapter.model.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import {
1010
ContactDelta,
1111
ContactTemplate,
1212
ContactUpdate,
13+
FollowUpWithIntegrationEntities,
1314
LabeledIntegrationEntity,
1415
LoggedIntegrationEntity,
16+
Task,
1517
} from '.';
1618
import { IntegrationEntityType } from './integration-entity.model';
1719
import { IntegrationsEvent } from './integrations-event.model';
@@ -91,4 +93,9 @@ export interface Adapter {
9193
) => Promise<{ apiKey: string; apiUrl: string }>;
9294
handleWebhook?: (req: Request) => Promise<IntegrationsEvent[]>;
9395
verifyWebhookRequest?: (req: Request) => Promise<boolean>;
96+
getTasks?: (req: Request, config: Config) => Promise<Task[]>;
97+
createFollowUp?: (
98+
config: Config,
99+
body: FollowUpWithIntegrationEntities,
100+
) => Promise<string>;
94101
}

src/models/follow-up.model.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { IntegrationEntity } from './integration-entity.model';
2+
3+
export type FollowUpEvent = {
4+
content: string;
5+
dueAt: number;
6+
title: string;
7+
type: string;
8+
};
9+
10+
export type FollowUpWithIntegrationEntities = FollowUpEvent & {
11+
integrationEntities: IntegrationEntity[];
12+
};

src/models/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,5 @@ export * from './storage-adapter.model';
2121
export * from './token-cache.model';
2222
export * from './token.model';
2323
export * from './user.model';
24+
export * from './follow-up.model';
25+
export * from './task.model';

src/models/task.model.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export type Task = {
2+
id: string;
3+
content: string;
4+
createdAt: Date;
5+
dueAt: Date;
6+
link?: string;
7+
title: string;
8+
};

0 commit comments

Comments
 (0)