@thalorlabs/jokeapi 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/JokeApiClient.d.ts +5 -7
- package/dist/JokeApiClient.js +12 -16
- package/package.json +3 -3
- package/src/JokeApiClient.ts +17 -15
- package/tests/JokeApiClient.test.ts +77 -129
package/dist/JokeApiClient.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BaseHttpClient } from '@thalorlabs/api';
|
|
1
2
|
import { TLJoke, EJokeType, EJokeCategory } from '@thalorlabs/types';
|
|
2
3
|
export interface JokeApiClientConfig {
|
|
3
4
|
baseURL?: string;
|
|
@@ -11,16 +12,13 @@ export interface JokeApiQueryParams {
|
|
|
11
12
|
/**
|
|
12
13
|
* HTTP adapter for v2.jokeapi.dev.
|
|
13
14
|
*
|
|
14
|
-
*
|
|
15
|
-
* Supports both single and twopart jokes, category
|
|
15
|
+
* Extends BaseHttpClient for retry logic, exponential backoff, and
|
|
16
|
+
* observability. Supports both single and twopart jokes, category
|
|
17
|
+
* filtering, and safe mode. Normalises the raw response to TLJoke.
|
|
16
18
|
* No DB, no cache, no logging — pure HTTP adapter.
|
|
17
|
-
*
|
|
18
|
-
* When @thalorlabs/api is available, this should extend BaseHttpClient
|
|
19
|
-
* instead of managing its own axios instance.
|
|
20
19
|
*/
|
|
21
|
-
export declare class JokeApiClient {
|
|
20
|
+
export declare class JokeApiClient extends BaseHttpClient {
|
|
22
21
|
readonly serviceName = "jokeapi";
|
|
23
|
-
private readonly axiosInstance;
|
|
24
22
|
constructor(config?: JokeApiClientConfig);
|
|
25
23
|
getJoke(params?: JokeApiQueryParams): Promise<TLJoke>;
|
|
26
24
|
private normalise;
|
package/dist/JokeApiClient.js
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.JokeApiClient = void 0;
|
|
7
|
-
const axios_1 = __importDefault(require("axios"));
|
|
8
4
|
const crypto_1 = require("crypto");
|
|
5
|
+
const api_1 = require("@thalorlabs/api");
|
|
9
6
|
const types_1 = require("@thalorlabs/types");
|
|
10
7
|
/** Maps TL categories to JokeAPI v2 category strings. */
|
|
11
8
|
const CATEGORY_MAP = {
|
|
@@ -29,23 +26,20 @@ const REVERSE_CATEGORY_MAP = {
|
|
|
29
26
|
/**
|
|
30
27
|
* HTTP adapter for v2.jokeapi.dev.
|
|
31
28
|
*
|
|
32
|
-
*
|
|
33
|
-
* Supports both single and twopart jokes, category
|
|
29
|
+
* Extends BaseHttpClient for retry logic, exponential backoff, and
|
|
30
|
+
* observability. Supports both single and twopart jokes, category
|
|
31
|
+
* filtering, and safe mode. Normalises the raw response to TLJoke.
|
|
34
32
|
* No DB, no cache, no logging — pure HTTP adapter.
|
|
35
|
-
*
|
|
36
|
-
* When @thalorlabs/api is available, this should extend BaseHttpClient
|
|
37
|
-
* instead of managing its own axios instance.
|
|
38
33
|
*/
|
|
39
|
-
class JokeApiClient {
|
|
34
|
+
class JokeApiClient extends api_1.BaseHttpClient {
|
|
40
35
|
constructor(config = {}) {
|
|
41
|
-
|
|
42
|
-
this.axiosInstance = axios_1.default.create({
|
|
36
|
+
super({
|
|
43
37
|
baseURL: config.baseURL ?? 'https://v2.jokeapi.dev',
|
|
38
|
+
serviceName: 'jokeapi',
|
|
39
|
+
retries: 3,
|
|
44
40
|
timeout: config.timeout ?? 10000,
|
|
45
|
-
headers: {
|
|
46
|
-
Accept: 'application/json',
|
|
47
|
-
},
|
|
48
41
|
});
|
|
42
|
+
this.serviceName = 'jokeapi';
|
|
49
43
|
}
|
|
50
44
|
async getJoke(params = {}) {
|
|
51
45
|
const category = params.category
|
|
@@ -59,7 +53,9 @@ class JokeApiClient {
|
|
|
59
53
|
if (params.safe) {
|
|
60
54
|
queryParams.safe = 'true';
|
|
61
55
|
}
|
|
62
|
-
const
|
|
56
|
+
const url = `/joke/${category}`;
|
|
57
|
+
const { response } = await this.handleRequest({ method: 'GET', url, params: queryParams }, { method: 'GET', url, requestId: (0, crypto_1.randomUUID)() });
|
|
58
|
+
const data = response.data;
|
|
63
59
|
if (data.error) {
|
|
64
60
|
throw new Error(`JokeAPI error: ${data.message}`);
|
|
65
61
|
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@thalorlabs/jokeapi",
|
|
3
3
|
"author": "ThalorLabs",
|
|
4
4
|
"private": false,
|
|
5
|
-
"version": "1.0
|
|
5
|
+
"version": "1.1.0",
|
|
6
6
|
"description": "Provider adapter for v2.jokeapi.dev — returns TLJoke",
|
|
7
7
|
"homepage": "https://github.com/ThalorLabs/jokeapi#readme",
|
|
8
8
|
"bugs": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"vitest": "^3.0.0"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@thalorlabs/
|
|
44
|
-
"
|
|
43
|
+
"@thalorlabs/api": "^1.0.0",
|
|
44
|
+
"@thalorlabs/types": "^1.7.0"
|
|
45
45
|
}
|
|
46
46
|
}
|
package/src/JokeApiClient.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { createHash, randomUUID } from 'crypto';
|
|
2
|
+
import { BaseHttpClient } from '@thalorlabs/api';
|
|
3
3
|
import { TLJoke, EJokeType, EJokeCategory } from '@thalorlabs/types';
|
|
4
4
|
import { JokeApiRawResponse, JokeApiErrorResponse } from './JokeApiTypes';
|
|
5
5
|
|
|
@@ -38,24 +38,20 @@ const REVERSE_CATEGORY_MAP: Record<string, EJokeCategory> = {
|
|
|
38
38
|
/**
|
|
39
39
|
* HTTP adapter for v2.jokeapi.dev.
|
|
40
40
|
*
|
|
41
|
-
*
|
|
42
|
-
* Supports both single and twopart jokes, category
|
|
41
|
+
* Extends BaseHttpClient for retry logic, exponential backoff, and
|
|
42
|
+
* observability. Supports both single and twopart jokes, category
|
|
43
|
+
* filtering, and safe mode. Normalises the raw response to TLJoke.
|
|
43
44
|
* No DB, no cache, no logging — pure HTTP adapter.
|
|
44
|
-
*
|
|
45
|
-
* When @thalorlabs/api is available, this should extend BaseHttpClient
|
|
46
|
-
* instead of managing its own axios instance.
|
|
47
45
|
*/
|
|
48
|
-
export class JokeApiClient {
|
|
46
|
+
export class JokeApiClient extends BaseHttpClient {
|
|
49
47
|
public readonly serviceName = 'jokeapi';
|
|
50
|
-
private readonly axiosInstance: AxiosInstance;
|
|
51
48
|
|
|
52
49
|
constructor(config: JokeApiClientConfig = {}) {
|
|
53
|
-
|
|
50
|
+
super({
|
|
54
51
|
baseURL: config.baseURL ?? 'https://v2.jokeapi.dev',
|
|
52
|
+
serviceName: 'jokeapi',
|
|
53
|
+
retries: 3,
|
|
55
54
|
timeout: config.timeout ?? 10000,
|
|
56
|
-
headers: {
|
|
57
|
-
Accept: 'application/json',
|
|
58
|
-
},
|
|
59
55
|
});
|
|
60
56
|
}
|
|
61
57
|
|
|
@@ -75,9 +71,15 @@ export class JokeApiClient {
|
|
|
75
71
|
queryParams.safe = 'true';
|
|
76
72
|
}
|
|
77
73
|
|
|
78
|
-
const
|
|
74
|
+
const url = `/joke/${category}`;
|
|
75
|
+
const { response } = await this.handleRequest<
|
|
79
76
|
JokeApiRawResponse | JokeApiErrorResponse
|
|
80
|
-
>(
|
|
77
|
+
>(
|
|
78
|
+
{ method: 'GET', url, params: queryParams },
|
|
79
|
+
{ method: 'GET', url, requestId: randomUUID() }
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const data = response.data;
|
|
81
83
|
|
|
82
84
|
if (data.error) {
|
|
83
85
|
throw new Error(
|
|
@@ -1,24 +1,12 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
import { JokeApiClient } from '../src/JokeApiClient';
|
|
3
3
|
import { EJokeType, EJokeCategory } from '@thalorlabs/types';
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
vi.mock('axios', () => {
|
|
7
|
-
const mockAxiosInstance = {
|
|
8
|
-
get: vi.fn(),
|
|
9
|
-
};
|
|
10
|
-
return {
|
|
11
|
-
default: {
|
|
12
|
-
create: vi.fn(() => mockAxiosInstance),
|
|
13
|
-
},
|
|
14
|
-
};
|
|
15
|
-
});
|
|
4
|
+
import { BaseHttpClient } from '@thalorlabs/api';
|
|
16
5
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
6
|
+
const mockHandleRequest = vi.fn();
|
|
7
|
+
vi.spyOn(BaseHttpClient.prototype as any, 'handleRequest').mockImplementation(
|
|
8
|
+
mockHandleRequest
|
|
9
|
+
);
|
|
22
10
|
|
|
23
11
|
describe('JokeApiClient', () => {
|
|
24
12
|
let client: JokeApiClient;
|
|
@@ -32,37 +20,24 @@ describe('JokeApiClient', () => {
|
|
|
32
20
|
it('sets serviceName to jokeapi', () => {
|
|
33
21
|
expect(client.serviceName).toBe('jokeapi');
|
|
34
22
|
});
|
|
35
|
-
|
|
36
|
-
it('creates axios instance with default config', () => {
|
|
37
|
-
expect(axios.create).toHaveBeenCalledWith({
|
|
38
|
-
baseURL: 'https://v2.jokeapi.dev',
|
|
39
|
-
timeout: 10000,
|
|
40
|
-
headers: { Accept: 'application/json' },
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
23
|
});
|
|
44
24
|
|
|
45
25
|
describe('getJoke — single joke', () => {
|
|
46
26
|
it('normalises a single joke to TLJoke', async () => {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
racist: false,
|
|
59
|
-
sexist: false,
|
|
60
|
-
explicit: false,
|
|
27
|
+
mockHandleRequest.mockResolvedValueOnce({
|
|
28
|
+
response: {
|
|
29
|
+
data: {
|
|
30
|
+
error: false,
|
|
31
|
+
category: 'Programming',
|
|
32
|
+
type: 'single',
|
|
33
|
+
joke: 'A SQL query walks into a bar.',
|
|
34
|
+
flags: { nsfw: false, religious: false, political: false, racist: false, sexist: false, explicit: false },
|
|
35
|
+
id: 42,
|
|
36
|
+
safe: true,
|
|
37
|
+
lang: 'en',
|
|
61
38
|
},
|
|
62
|
-
id: 42,
|
|
63
|
-
safe: true,
|
|
64
|
-
lang: 'en',
|
|
65
39
|
},
|
|
40
|
+
durationMs: 50,
|
|
66
41
|
});
|
|
67
42
|
|
|
68
43
|
const joke = await client.getJoke();
|
|
@@ -79,26 +54,21 @@ describe('JokeApiClient', () => {
|
|
|
79
54
|
|
|
80
55
|
describe('getJoke — twopart joke', () => {
|
|
81
56
|
it('normalises a twopart joke to TLJoke', async () => {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
racist: false,
|
|
95
|
-
sexist: false,
|
|
96
|
-
explicit: false,
|
|
57
|
+
mockHandleRequest.mockResolvedValueOnce({
|
|
58
|
+
response: {
|
|
59
|
+
data: {
|
|
60
|
+
error: false,
|
|
61
|
+
category: 'Programming',
|
|
62
|
+
type: 'twopart',
|
|
63
|
+
setup: 'Why do programmers prefer dark mode?',
|
|
64
|
+
delivery: 'Because light attracts bugs.',
|
|
65
|
+
flags: { nsfw: false, religious: false, political: false, racist: false, sexist: false, explicit: false },
|
|
66
|
+
id: 58,
|
|
67
|
+
safe: true,
|
|
68
|
+
lang: 'en',
|
|
97
69
|
},
|
|
98
|
-
id: 58,
|
|
99
|
-
safe: true,
|
|
100
|
-
lang: 'en',
|
|
101
70
|
},
|
|
71
|
+
durationMs: 50,
|
|
102
72
|
});
|
|
103
73
|
|
|
104
74
|
const joke = await client.getJoke();
|
|
@@ -114,104 +84,83 @@ describe('JokeApiClient', () => {
|
|
|
114
84
|
|
|
115
85
|
describe('getJoke — category mapping', () => {
|
|
116
86
|
it('maps PROGRAMMING category to Programming endpoint', async () => {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
joke: 'A joke',
|
|
124
|
-
flags: {
|
|
125
|
-
nsfw: false,
|
|
126
|
-
religious: false,
|
|
127
|
-
political: false,
|
|
128
|
-
racist: false,
|
|
129
|
-
sexist: false,
|
|
130
|
-
explicit: false,
|
|
87
|
+
mockHandleRequest.mockResolvedValueOnce({
|
|
88
|
+
response: {
|
|
89
|
+
data: {
|
|
90
|
+
error: false, category: 'Programming', type: 'single', joke: 'A joke',
|
|
91
|
+
flags: { nsfw: false, religious: false, political: false, racist: false, sexist: false, explicit: false },
|
|
92
|
+
id: 1, safe: true, lang: 'en',
|
|
131
93
|
},
|
|
132
|
-
id: 1,
|
|
133
|
-
safe: true,
|
|
134
|
-
lang: 'en',
|
|
135
94
|
},
|
|
95
|
+
durationMs: 10,
|
|
136
96
|
});
|
|
137
97
|
|
|
138
98
|
await client.getJoke({ category: EJokeCategory.PROGRAMMING });
|
|
139
99
|
|
|
140
|
-
expect(
|
|
100
|
+
expect(mockHandleRequest).toHaveBeenCalledWith(
|
|
101
|
+
expect.objectContaining({ url: '/joke/Programming' }),
|
|
102
|
+
expect.objectContaining({ url: '/joke/Programming' })
|
|
103
|
+
);
|
|
141
104
|
});
|
|
142
105
|
|
|
143
106
|
it('uses Any when no category specified', async () => {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
joke: 'A joke',
|
|
151
|
-
flags: {
|
|
152
|
-
nsfw: false,
|
|
153
|
-
religious: false,
|
|
154
|
-
political: false,
|
|
155
|
-
racist: false,
|
|
156
|
-
sexist: false,
|
|
157
|
-
explicit: false,
|
|
107
|
+
mockHandleRequest.mockResolvedValueOnce({
|
|
108
|
+
response: {
|
|
109
|
+
data: {
|
|
110
|
+
error: false, category: 'Misc', type: 'single', joke: 'A joke',
|
|
111
|
+
flags: { nsfw: false, religious: false, political: false, racist: false, sexist: false, explicit: false },
|
|
112
|
+
id: 1, safe: true, lang: 'en',
|
|
158
113
|
},
|
|
159
|
-
id: 1,
|
|
160
|
-
safe: true,
|
|
161
|
-
lang: 'en',
|
|
162
114
|
},
|
|
115
|
+
durationMs: 10,
|
|
163
116
|
});
|
|
164
117
|
|
|
165
118
|
await client.getJoke();
|
|
166
119
|
|
|
167
|
-
expect(
|
|
120
|
+
expect(mockHandleRequest).toHaveBeenCalledWith(
|
|
121
|
+
expect.objectContaining({ url: '/joke/Any' }),
|
|
122
|
+
expect.objectContaining({ url: '/joke/Any' })
|
|
123
|
+
);
|
|
168
124
|
});
|
|
169
125
|
});
|
|
170
126
|
|
|
171
127
|
describe('getJoke — safe mode', () => {
|
|
172
128
|
it('passes safe=true query param when requested', async () => {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
joke: 'A joke',
|
|
180
|
-
flags: {
|
|
181
|
-
nsfw: false,
|
|
182
|
-
religious: false,
|
|
183
|
-
political: false,
|
|
184
|
-
racist: false,
|
|
185
|
-
sexist: false,
|
|
186
|
-
explicit: false,
|
|
129
|
+
mockHandleRequest.mockResolvedValueOnce({
|
|
130
|
+
response: {
|
|
131
|
+
data: {
|
|
132
|
+
error: false, category: 'Misc', type: 'single', joke: 'A joke',
|
|
133
|
+
flags: { nsfw: false, religious: false, political: false, racist: false, sexist: false, explicit: false },
|
|
134
|
+
id: 1, safe: true, lang: 'en',
|
|
187
135
|
},
|
|
188
|
-
id: 1,
|
|
189
|
-
safe: true,
|
|
190
|
-
lang: 'en',
|
|
191
136
|
},
|
|
137
|
+
durationMs: 10,
|
|
192
138
|
});
|
|
193
139
|
|
|
194
140
|
await client.getJoke({ safe: true });
|
|
195
141
|
|
|
196
|
-
expect(
|
|
197
|
-
params: { safe: 'true' },
|
|
198
|
-
|
|
142
|
+
expect(mockHandleRequest).toHaveBeenCalledWith(
|
|
143
|
+
expect.objectContaining({ params: { safe: 'true' } }),
|
|
144
|
+
expect.anything()
|
|
145
|
+
);
|
|
199
146
|
});
|
|
200
147
|
});
|
|
201
148
|
|
|
202
149
|
describe('getJoke — error handling', () => {
|
|
203
150
|
it('throws on JokeAPI error response', async () => {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
151
|
+
mockHandleRequest.mockResolvedValueOnce({
|
|
152
|
+
response: {
|
|
153
|
+
data: {
|
|
154
|
+
error: true,
|
|
155
|
+
internalError: false,
|
|
156
|
+
code: 106,
|
|
157
|
+
message: 'No matching joke found',
|
|
158
|
+
causedBy: ['No jokes found'],
|
|
159
|
+
additionalInfo: '',
|
|
160
|
+
timestamp: Date.now(),
|
|
161
|
+
},
|
|
214
162
|
},
|
|
163
|
+
durationMs: 10,
|
|
215
164
|
});
|
|
216
165
|
|
|
217
166
|
await expect(client.getJoke()).rejects.toThrow(
|
|
@@ -219,9 +168,8 @@ describe('JokeApiClient', () => {
|
|
|
219
168
|
);
|
|
220
169
|
});
|
|
221
170
|
|
|
222
|
-
it('propagates
|
|
223
|
-
|
|
224
|
-
mockGet.mockRejectedValueOnce(new Error('Network error'));
|
|
171
|
+
it('propagates errors from handleRequest', async () => {
|
|
172
|
+
mockHandleRequest.mockRejectedValueOnce(new Error('Network error'));
|
|
225
173
|
|
|
226
174
|
await expect(client.getJoke()).rejects.toThrow('Network error');
|
|
227
175
|
});
|