@thalorlabs/dadjoke 1.0.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/.env.example ADDED
@@ -0,0 +1,6 @@
1
+ # @thalorlabs/dadjoke
2
+ # No environment variables required — icanhazdadjoke.com is free and unauthenticated.
3
+ # This file exists for consistency with other provider packages.
4
+
5
+ # Optional: Override the API base URL (e.g. for testing with a mock server)
6
+ # DAD_JOKE_BASE_URL=https://icanhazdadjoke.com
package/.prettierrc ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "semi": true,
3
+ "singleQuote": true,
4
+ "trailingComma": "es5",
5
+ "printWidth": 80,
6
+ "tabWidth": 2
7
+ }
package/CLAUDE.md ADDED
@@ -0,0 +1,52 @@
1
+ # @thalorlabs/dadjoke
2
+
3
+ ## What this repo is
4
+
5
+ Provider adapter package for icanhazdadjoke.com. Calls one external API, normalises the response to `TLJoke`, and returns it. No DB, no cache, no logging.
6
+
7
+ ## Knowledge base
8
+
9
+ Before writing any code, read:
10
+ - TL-Coding vault: `knowledge/multi-provider-pattern.md`
11
+ - TL-Coding vault: `examples/jokes/layer-provider-package.md`
12
+ - TL-Coding vault: `context/packages.md`
13
+
14
+ ## External API
15
+
16
+ - **API:** icanhazdadjoke.com
17
+ - **Auth:** None
18
+ - **Billable:** No
19
+ - **Joke type:** Always SINGLE (one-liner)
20
+ - **Category:** Always DAD
21
+
22
+ ## Build & publish
23
+
24
+ ```bash
25
+ npm run build # tsc → dist/
26
+ npm test # vitest
27
+ npm version patch # or minor/major
28
+ npm publish --access public
29
+ ```
30
+
31
+ ## Folder structure
32
+
33
+ ```
34
+ src/
35
+ index.ts ← exports DadJokeClient, DAD_JOKE_CONFIG
36
+ DadJokeClient.ts ← HTTP adapter, normalises to TLJoke
37
+ DadJokeTypes.ts ← raw API types, local only, never exported
38
+ tests/
39
+ DadJokeClient.test.ts
40
+ ```
41
+
42
+ ## What this package does NOT do
43
+
44
+ - No database access
45
+ - No caching (orchestrator handles this)
46
+ - No logging (orchestrator handles this)
47
+ - Raw types (`DadJokeRawResponse`) never exported, never in `@thalorlabs/types`
48
+ - No provider selection logic — that's the orchestrator's job
49
+
50
+ ## Migration note
51
+
52
+ When `@thalorlabs/api` is published, `DadJokeClient` should extend `BaseHttpClient` instead of managing its own axios instance. This gives us retry logic, exponential backoff, and observability for free.
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # @thalorlabs/dadjoke
2
+
3
+ Provider adapter for [icanhazdadjoke.com](https://icanhazdadjoke.com). Returns normalised `TLJoke` from `@thalorlabs/types`.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @thalorlabs/dadjoke
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { DadJokeClient, DAD_JOKE_CONFIG } from '@thalorlabs/dadjoke';
15
+
16
+ const client = new DadJokeClient();
17
+ const joke = await client.getJoke();
18
+ // → TLJoke { id, type: 'SINGLE', joke: '...', category: 'DAD', safe: true, provider: 'dadjoke' }
19
+ ```
20
+
21
+ ## Configuration
22
+
23
+ | Option | Default | Description |
24
+ |--------|---------|-------------|
25
+ | `baseURL` | `https://icanhazdadjoke.com` | API base URL |
26
+ | `timeout` | `10000` | Request timeout (ms) |
27
+
28
+ ## Exported Config
29
+
30
+ ```typescript
31
+ DAD_JOKE_CONFIG = {
32
+ cacheTtlMs: 86400000, // 24h
33
+ isBillable: false,
34
+ }
35
+ ```
36
+
37
+ ## Scripts
38
+
39
+ ```bash
40
+ npm run build # tsc → dist/
41
+ npm test # vitest
42
+ npm run lint # eslint
43
+ npm run format:check # prettier
44
+ ```
@@ -0,0 +1,21 @@
1
+ import { TLJoke } from '@thalorlabs/types';
2
+ export interface DadJokeClientConfig {
3
+ baseURL?: string;
4
+ timeout?: number;
5
+ }
6
+ /**
7
+ * HTTP adapter for icanhazdadjoke.com.
8
+ *
9
+ * Calls the external API and normalises the raw response to TLJoke.
10
+ * 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
+ */
15
+ export declare class DadJokeClient {
16
+ readonly serviceName = "dadjoke";
17
+ private readonly axiosInstance;
18
+ constructor(config?: DadJokeClientConfig);
19
+ getJoke(): Promise<TLJoke>;
20
+ private normalise;
21
+ }
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DadJokeClient = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ const crypto_1 = require("crypto");
9
+ const types_1 = require("@thalorlabs/types");
10
+ /**
11
+ * HTTP adapter for icanhazdadjoke.com.
12
+ *
13
+ * Calls the external API and normalises the raw response to TLJoke.
14
+ * 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
+ */
19
+ class DadJokeClient {
20
+ constructor(config = {}) {
21
+ this.serviceName = 'dadjoke';
22
+ this.axiosInstance = axios_1.default.create({
23
+ baseURL: config.baseURL ?? 'https://icanhazdadjoke.com',
24
+ timeout: config.timeout ?? 10000,
25
+ headers: {
26
+ Accept: 'application/json',
27
+ },
28
+ });
29
+ }
30
+ async getJoke() {
31
+ const { data } = await this.axiosInstance.get('/');
32
+ return this.normalise(data);
33
+ }
34
+ normalise(raw) {
35
+ return types_1.TLJoke.parse({
36
+ id: (0, crypto_1.createHash)('md5').update(raw.joke).digest('hex'),
37
+ type: types_1.EJokeType.SINGLE,
38
+ joke: raw.joke,
39
+ category: types_1.EJokeCategory.DAD,
40
+ safe: true,
41
+ provider: this.serviceName,
42
+ });
43
+ }
44
+ }
45
+ exports.DadJokeClient = DadJokeClient;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Raw response from icanhazdadjoke.com API.
3
+ *
4
+ * Internal to this package only — never exported from index.ts,
5
+ * never added to @thalorlabs/types.
6
+ */
7
+ export interface DadJokeRawResponse {
8
+ id: string;
9
+ joke: string;
10
+ status: number;
11
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,5 @@
1
+ export { DadJokeClient, DadJokeClientConfig } from './DadJokeClient';
2
+ export declare const DAD_JOKE_CONFIG: {
3
+ readonly cacheTtlMs: number;
4
+ readonly isBillable: false;
5
+ };
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DAD_JOKE_CONFIG = exports.DadJokeClient = void 0;
4
+ var DadJokeClient_1 = require("./DadJokeClient");
5
+ Object.defineProperty(exports, "DadJokeClient", { enumerable: true, get: function () { return DadJokeClient_1.DadJokeClient; } });
6
+ exports.DAD_JOKE_CONFIG = {
7
+ cacheTtlMs: 1000 * 60 * 60 * 24, // 24h — jokes don't change
8
+ isBillable: false, // icanhazdadjoke is free
9
+ };
@@ -0,0 +1,21 @@
1
+ import eslint from '@eslint/js';
2
+ import tseslint from 'typescript-eslint';
3
+ import prettier from 'eslint-config-prettier';
4
+ import nodePlugin from 'eslint-plugin-n';
5
+
6
+ export default tseslint.config(
7
+ eslint.configs.recommended,
8
+ ...tseslint.configs.recommended,
9
+ nodePlugin.configs['flat/recommended-module'],
10
+ prettier,
11
+ {
12
+ rules: {
13
+ '@typescript-eslint/no-explicit-any': 'error',
14
+ 'n/no-missing-import': 'off', // TypeScript handles this
15
+ 'n/no-unpublished-import': 'off',
16
+ },
17
+ },
18
+ {
19
+ ignores: ['dist/', 'node_modules/'],
20
+ }
21
+ );
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@thalorlabs/dadjoke",
3
+ "author": "ThalorLabs",
4
+ "private": false,
5
+ "version": "1.0.0",
6
+ "description": "Provider adapter for icanhazdadjoke.com — returns TLJoke",
7
+ "homepage": "https://github.com/ThalorLabs/dadjoke#readme",
8
+ "bugs": {
9
+ "url": "https://github.com/ThalorLabs/dadjoke/issues"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/ThalorLabs/dadjoke.git"
14
+ },
15
+ "license": "ISC",
16
+ "main": "dist/index.js",
17
+ "types": "dist/index.d.ts",
18
+ "scripts": {
19
+ "test": "vitest run",
20
+ "test:watch": "vitest",
21
+ "build": "tsc",
22
+ "clean": "rimraf dist",
23
+ "prebuild": "npm run clean",
24
+ "prepublishOnly": "npm run build",
25
+ "lint": "eslint src/",
26
+ "lint:fix": "eslint src/ --fix",
27
+ "format": "prettier --write \"src/**/*.ts\"",
28
+ "format:check": "prettier --check \"src/**/*.ts\""
29
+ },
30
+ "devDependencies": {
31
+ "@eslint/js": "^10.0.1",
32
+ "@types/node": "^25.5.2",
33
+ "eslint": "^10.1.0",
34
+ "eslint-config-prettier": "^10.1.8",
35
+ "eslint-plugin-n": "^17.24.0",
36
+ "prettier": "^3.8.1",
37
+ "rimraf": "^5.0.0",
38
+ "typescript": "^5.0.0",
39
+ "typescript-eslint": "^8.58.0",
40
+ "vitest": "^3.0.0"
41
+ },
42
+ "dependencies": {
43
+ "@thalorlabs/types": "^1.7.0",
44
+ "axios": "^1.7.0"
45
+ }
46
+ }
@@ -0,0 +1,49 @@
1
+ import axios, { AxiosInstance } from 'axios';
2
+ import { createHash } from 'crypto';
3
+ import { TLJoke, EJokeType, EJokeCategory } from '@thalorlabs/types';
4
+ import { DadJokeRawResponse } from './DadJokeTypes';
5
+
6
+ export interface DadJokeClientConfig {
7
+ baseURL?: string;
8
+ timeout?: number;
9
+ }
10
+
11
+ /**
12
+ * HTTP adapter for icanhazdadjoke.com.
13
+ *
14
+ * Calls the external API and normalises the raw response to TLJoke.
15
+ * 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
+ */
20
+ export class DadJokeClient {
21
+ public readonly serviceName = 'dadjoke';
22
+ private readonly axiosInstance: AxiosInstance;
23
+
24
+ constructor(config: DadJokeClientConfig = {}) {
25
+ this.axiosInstance = axios.create({
26
+ baseURL: config.baseURL ?? 'https://icanhazdadjoke.com',
27
+ timeout: config.timeout ?? 10000,
28
+ headers: {
29
+ Accept: 'application/json',
30
+ },
31
+ });
32
+ }
33
+
34
+ async getJoke(): Promise<TLJoke> {
35
+ const { data } = await this.axiosInstance.get<DadJokeRawResponse>('/');
36
+ return this.normalise(data);
37
+ }
38
+
39
+ private normalise(raw: DadJokeRawResponse): TLJoke {
40
+ return TLJoke.parse({
41
+ id: createHash('md5').update(raw.joke).digest('hex'),
42
+ type: EJokeType.SINGLE,
43
+ joke: raw.joke,
44
+ category: EJokeCategory.DAD,
45
+ safe: true,
46
+ provider: this.serviceName,
47
+ });
48
+ }
49
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Raw response from icanhazdadjoke.com API.
3
+ *
4
+ * Internal to this package only — never exported from index.ts,
5
+ * never added to @thalorlabs/types.
6
+ */
7
+ export interface DadJokeRawResponse {
8
+ id: string;
9
+ joke: string;
10
+ status: number;
11
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { DadJokeClient, DadJokeClientConfig } from './DadJokeClient';
2
+
3
+ export const DAD_JOKE_CONFIG = {
4
+ cacheTtlMs: 1000 * 60 * 60 * 24, // 24h — jokes don't change
5
+ isBillable: false, // icanhazdadjoke is free
6
+ } as const;
@@ -0,0 +1,109 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { DadJokeClient } from '../src/DadJokeClient';
3
+ import { EJokeType, EJokeCategory } from '@thalorlabs/types';
4
+ import axios from 'axios';
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
+ }
22
+
23
+ describe('DadJokeClient', () => {
24
+ let client: DadJokeClient;
25
+
26
+ beforeEach(() => {
27
+ vi.clearAllMocks();
28
+ client = new DadJokeClient();
29
+ });
30
+
31
+ describe('constructor', () => {
32
+ it('sets serviceName to dadjoke', () => {
33
+ expect(client.serviceName).toBe('dadjoke');
34
+ });
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
+ });
54
+
55
+ describe('getJoke', () => {
56
+ 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,
63
+ },
64
+ });
65
+
66
+ const joke = await client.getJoke();
67
+
68
+ expect(joke.type).toBe(EJokeType.SINGLE);
69
+ expect(joke.category).toBe(EJokeCategory.DAD);
70
+ expect(joke.joke).toBe(
71
+ 'Why did the scarecrow win an award? He was outstanding in his field.'
72
+ );
73
+ expect(joke.safe).toBe(true);
74
+ expect(joke.provider).toBe('dadjoke');
75
+ expect(joke.id).toBeDefined();
76
+ });
77
+
78
+ it('generates a deterministic id from joke text', async () => {
79
+ const mockGet = getMockAxios();
80
+ const jokeText = 'Test joke';
81
+ mockGet.mockResolvedValue({
82
+ data: { id: 'raw-id', joke: jokeText, status: 200 },
83
+ });
84
+
85
+ const joke1 = await client.getJoke();
86
+ const joke2 = await client.getJoke();
87
+
88
+ expect(joke1.id).toBe(joke2.id);
89
+ });
90
+
91
+ it('calls GET / on the API', async () => {
92
+ const mockGet = getMockAxios();
93
+ mockGet.mockResolvedValueOnce({
94
+ data: { id: 'abc', joke: 'A joke', status: 200 },
95
+ });
96
+
97
+ await client.getJoke();
98
+
99
+ expect(mockGet).toHaveBeenCalledWith('/');
100
+ });
101
+
102
+ it('propagates API errors', async () => {
103
+ const mockGet = getMockAxios();
104
+ mockGet.mockRejectedValueOnce(new Error('Network error'));
105
+
106
+ await expect(client.getJoke()).rejects.toThrow('Network error');
107
+ });
108
+ });
109
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "CommonJS",
5
+ "declaration": true,
6
+ "outDir": "./dist",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "moduleResolution": "node"
11
+ },
12
+ "include": ["src"],
13
+ "exclude": ["node_modules", "dist"]
14
+ }