@thalorlabs/dadjoke 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.
@@ -1,3 +1,4 @@
1
+ import { BaseHttpClient } from '@thalorlabs/api';
1
2
  import { TLJoke } from '@thalorlabs/types';
2
3
  export interface DadJokeClientConfig {
3
4
  baseURL?: string;
@@ -6,15 +7,12 @@ export interface DadJokeClientConfig {
6
7
  /**
7
8
  * HTTP adapter for icanhazdadjoke.com.
8
9
  *
9
- * Calls the external API and normalises the raw response to TLJoke.
10
+ * Extends BaseHttpClient for retry logic, exponential backoff, and
11
+ * observability. Normalises the raw response to TLJoke.
10
12
  * No DB, no cache, no logging — pure HTTP adapter.
11
- *
12
- * When @thalorlabs/api is available, this should extend BaseHttpClient
13
- * instead of managing its own axios instance.
14
13
  */
15
- export declare class DadJokeClient {
14
+ export declare class DadJokeClient extends BaseHttpClient {
16
15
  readonly serviceName = "dadjoke";
17
- private readonly axiosInstance;
18
16
  constructor(config?: DadJokeClientConfig);
19
17
  getJoke(): Promise<TLJoke>;
20
18
  private normalise;
@@ -1,35 +1,29 @@
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.DadJokeClient = 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
  /**
11
8
  * HTTP adapter for icanhazdadjoke.com.
12
9
  *
13
- * Calls the external API and normalises the raw response to TLJoke.
10
+ * Extends BaseHttpClient for retry logic, exponential backoff, and
11
+ * observability. Normalises the raw response to TLJoke.
14
12
  * No DB, no cache, no logging — pure HTTP adapter.
15
- *
16
- * When @thalorlabs/api is available, this should extend BaseHttpClient
17
- * instead of managing its own axios instance.
18
13
  */
19
- class DadJokeClient {
14
+ class DadJokeClient extends api_1.BaseHttpClient {
20
15
  constructor(config = {}) {
21
- this.serviceName = 'dadjoke';
22
- this.axiosInstance = axios_1.default.create({
16
+ super({
23
17
  baseURL: config.baseURL ?? 'https://icanhazdadjoke.com',
18
+ serviceName: 'dadjoke',
19
+ retries: 3,
24
20
  timeout: config.timeout ?? 10000,
25
- headers: {
26
- Accept: 'application/json',
27
- },
28
21
  });
22
+ this.serviceName = 'dadjoke';
29
23
  }
30
24
  async getJoke() {
31
- const { data } = await this.axiosInstance.get('/');
32
- return this.normalise(data);
25
+ const { response } = await this.handleRequest({ method: 'GET', url: '/' }, { method: 'GET', url: '/', requestId: (0, crypto_1.randomUUID)() });
26
+ return this.normalise(response.data);
33
27
  }
34
28
  normalise(raw) {
35
29
  return types_1.TLJoke.parse({
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@thalorlabs/dadjoke",
3
3
  "author": "ThalorLabs",
4
4
  "private": false,
5
- "version": "1.0.1",
5
+ "version": "1.1.0",
6
6
  "description": "Provider adapter for icanhazdadjoke.com — returns TLJoke",
7
7
  "homepage": "https://github.com/ThalorLabs/dadjoke#readme",
8
8
  "bugs": {
@@ -40,7 +40,7 @@
40
40
  "vitest": "^3.0.0"
41
41
  },
42
42
  "dependencies": {
43
- "@thalorlabs/types": "^1.7.0",
44
- "axios": "^1.7.0"
43
+ "@thalorlabs/api": "^1.0.0",
44
+ "@thalorlabs/types": "^1.7.0"
45
45
  }
46
46
  }
@@ -1,5 +1,5 @@
1
- import axios, { AxiosInstance } from 'axios';
2
- import { createHash } from 'crypto';
1
+ import { createHash, randomUUID } from 'crypto';
2
+ import { BaseHttpClient } from '@thalorlabs/api';
3
3
  import { TLJoke, EJokeType, EJokeCategory } from '@thalorlabs/types';
4
4
  import { DadJokeRawResponse } from './DadJokeTypes';
5
5
 
@@ -11,29 +11,28 @@ export interface DadJokeClientConfig {
11
11
  /**
12
12
  * HTTP adapter for icanhazdadjoke.com.
13
13
  *
14
- * Calls the external API and normalises the raw response to TLJoke.
14
+ * Extends BaseHttpClient for retry logic, exponential backoff, and
15
+ * observability. Normalises the raw response to TLJoke.
15
16
  * No DB, no cache, no logging — pure HTTP adapter.
16
- *
17
- * When @thalorlabs/api is available, this should extend BaseHttpClient
18
- * instead of managing its own axios instance.
19
17
  */
20
- export class DadJokeClient {
18
+ export class DadJokeClient extends BaseHttpClient {
21
19
  public readonly serviceName = 'dadjoke';
22
- private readonly axiosInstance: AxiosInstance;
23
20
 
24
21
  constructor(config: DadJokeClientConfig = {}) {
25
- this.axiosInstance = axios.create({
22
+ super({
26
23
  baseURL: config.baseURL ?? 'https://icanhazdadjoke.com',
24
+ serviceName: 'dadjoke',
25
+ retries: 3,
27
26
  timeout: config.timeout ?? 10000,
28
- headers: {
29
- Accept: 'application/json',
30
- },
31
27
  });
32
28
  }
33
29
 
34
30
  async getJoke(): Promise<TLJoke> {
35
- const { data } = await this.axiosInstance.get<DadJokeRawResponse>('/');
36
- return this.normalise(data);
31
+ const { response } = await this.handleRequest<DadJokeRawResponse>(
32
+ { method: 'GET', url: '/' },
33
+ { method: 'GET', url: '/', requestId: randomUUID() }
34
+ );
35
+ return this.normalise(response.data);
37
36
  }
38
37
 
39
38
  private normalise(raw: DadJokeRawResponse): TLJoke {
@@ -1,24 +1,13 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import { DadJokeClient } from '../src/DadJokeClient';
3
3
  import { EJokeType, EJokeCategory } from '@thalorlabs/types';
4
- import axios from 'axios';
4
+ import { BaseHttpClient } from '@thalorlabs/api';
5
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
- });
16
-
17
- function getMockAxios() {
18
- const instance = (axios.create as ReturnType<typeof vi.fn>).mock.results[0]
19
- .value;
20
- return instance.get as ReturnType<typeof vi.fn>;
21
- }
6
+ // Mock handleRequest on the prototype — no axios import needed in tests
7
+ const mockHandleRequest = vi.fn();
8
+ vi.spyOn(BaseHttpClient.prototype as any, 'handleRequest').mockImplementation(
9
+ mockHandleRequest
10
+ );
22
11
 
23
12
  describe('DadJokeClient', () => {
24
13
  let client: DadJokeClient;
@@ -32,35 +21,19 @@ describe('DadJokeClient', () => {
32
21
  it('sets serviceName to dadjoke', () => {
33
22
  expect(client.serviceName).toBe('dadjoke');
34
23
  });
35
-
36
- it('creates axios instance with default config', () => {
37
- expect(axios.create).toHaveBeenCalledWith({
38
- baseURL: 'https://icanhazdadjoke.com',
39
- timeout: 10000,
40
- headers: { Accept: 'application/json' },
41
- });
42
- });
43
-
44
- it('accepts custom baseURL and timeout', () => {
45
- vi.clearAllMocks();
46
- new DadJokeClient({ baseURL: 'http://localhost:3000', timeout: 5000 });
47
- expect(axios.create).toHaveBeenCalledWith({
48
- baseURL: 'http://localhost:3000',
49
- timeout: 5000,
50
- headers: { Accept: 'application/json' },
51
- });
52
- });
53
24
  });
54
25
 
55
26
  describe('getJoke', () => {
56
27
  it('returns a valid TLJoke with SINGLE type and DAD category', async () => {
57
- const mockGet = getMockAxios();
58
- mockGet.mockResolvedValueOnce({
59
- data: {
60
- id: 'abc123',
61
- joke: 'Why did the scarecrow win an award? He was outstanding in his field.',
62
- status: 200,
28
+ mockHandleRequest.mockResolvedValueOnce({
29
+ response: {
30
+ data: {
31
+ id: 'abc123',
32
+ joke: 'Why did the scarecrow win an award? He was outstanding in his field.',
33
+ status: 200,
34
+ },
63
35
  },
36
+ durationMs: 50,
64
37
  });
65
38
 
66
39
  const joke = await client.getJoke();
@@ -76,10 +49,10 @@ describe('DadJokeClient', () => {
76
49
  });
77
50
 
78
51
  it('generates a deterministic id from joke text', async () => {
79
- const mockGet = getMockAxios();
80
52
  const jokeText = 'Test joke';
81
- mockGet.mockResolvedValue({
82
- data: { id: 'raw-id', joke: jokeText, status: 200 },
53
+ mockHandleRequest.mockResolvedValue({
54
+ response: { data: { id: 'raw-id', joke: jokeText, status: 200 } },
55
+ durationMs: 10,
83
56
  });
84
57
 
85
58
  const joke1 = await client.getJoke();
@@ -88,20 +61,22 @@ describe('DadJokeClient', () => {
88
61
  expect(joke1.id).toBe(joke2.id);
89
62
  });
90
63
 
91
- it('calls GET / on the API', async () => {
92
- const mockGet = getMockAxios();
93
- mockGet.mockResolvedValueOnce({
94
- data: { id: 'abc', joke: 'A joke', status: 200 },
64
+ it('calls handleRequest with GET /', async () => {
65
+ mockHandleRequest.mockResolvedValueOnce({
66
+ response: { data: { id: 'abc', joke: 'A joke', status: 200 } },
67
+ durationMs: 10,
95
68
  });
96
69
 
97
70
  await client.getJoke();
98
71
 
99
- expect(mockGet).toHaveBeenCalledWith('/');
72
+ expect(mockHandleRequest).toHaveBeenCalledWith(
73
+ expect.objectContaining({ method: 'GET', url: '/' }),
74
+ expect.objectContaining({ method: 'GET', url: '/' })
75
+ );
100
76
  });
101
77
 
102
- it('propagates API errors', async () => {
103
- const mockGet = getMockAxios();
104
- mockGet.mockRejectedValueOnce(new Error('Network error'));
78
+ it('propagates errors from handleRequest', async () => {
79
+ mockHandleRequest.mockRejectedValueOnce(new Error('Network error'));
105
80
 
106
81
  await expect(client.getJoke()).rejects.toThrow('Network error');
107
82
  });