@t4h.framework/http 0.3.1 → 0.4.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/.ai/skills/framework-http/SKILL.md +70 -137
- package/package.json +7 -7
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: framework-http
|
|
3
3
|
description: >-
|
|
4
|
-
Guides correct use of @t4h.framework/http in T4H Framework workflows:
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
activities, OAuth2/Basic auth, or working in packages/http.
|
|
4
|
+
Guides correct use of @t4h.framework/http in T4H Framework workflows: the
|
|
5
|
+
`http` / `HttpClient` client, claims (HttpClientRequestClaim, FileSystemClaim,
|
|
6
|
+
CacheClaim for OAuth), request/response bodies, and auth. Use when making or
|
|
7
|
+
testing HTTP calls in workflows, wiring HTTP claim providers, OAuth2/Basic
|
|
8
|
+
auth, or working in packages/http.
|
|
10
9
|
---
|
|
11
10
|
|
|
12
11
|
# @t4h.framework/http
|
|
@@ -19,8 +18,6 @@ Import surface (from `src/http.ts` only — do not reference unexported modules
|
|
|
19
18
|
import {
|
|
20
19
|
http,
|
|
21
20
|
HttpClient,
|
|
22
|
-
HttpRequestActivity,
|
|
23
|
-
AbstractHttpRequestActivity,
|
|
24
21
|
HttpClientRequestClaim,
|
|
25
22
|
HttpAuth,
|
|
26
23
|
OAuth2,
|
|
@@ -28,13 +25,15 @@ import {
|
|
|
28
25
|
OAuth2GrantResourceOwnerPassword,
|
|
29
26
|
Body,
|
|
30
27
|
Response,
|
|
31
|
-
type HttpRequestActivityInput,
|
|
32
|
-
type HttpRequestActivityOutput,
|
|
33
28
|
type HttpClientRequestClaimInput,
|
|
34
29
|
type HttpClientRequestClaimOutput,
|
|
35
30
|
} from '@t4h.framework/http'
|
|
36
31
|
```
|
|
37
32
|
|
|
33
|
+
> Workflows use the **`http` / `HttpClient`** client. The package also exports
|
|
34
|
+
> the underlying `HttpRequestActivity` / `AbstractHttpRequestActivity`, but
|
|
35
|
+
> workflow code never imports, instantiates, or extends them.
|
|
36
|
+
|
|
38
37
|
## Architecture
|
|
39
38
|
|
|
40
39
|
`Workflow` → `http`/`HttpClient` (`History.bind(HttpRequestActivity)`) → `AbstractHttpRequestActivity.run` → `HttpClientRequestClaim.request` + `FileSystemClaim.write` (response → `Body`/`Response`).
|
|
@@ -78,11 +77,11 @@ abstract class HttpClientRequestClaim {
|
|
|
78
77
|
}
|
|
79
78
|
```
|
|
80
79
|
|
|
81
|
-
|
|
80
|
+
The runtime provides a concrete implementation; workflow tests mock it (see **Testing** below).
|
|
82
81
|
|
|
83
|
-
### FileSystemClaim (required by
|
|
82
|
+
### FileSystemClaim (required by the HTTP activity)
|
|
84
83
|
|
|
85
|
-
|
|
84
|
+
The HTTP activity persists the response body to `fs` (path `\`${uuid}-http-request-activity-response-body.${ext}\``, extension from `content-type` via `mime`, or `.bin`) and returns it wrapped in a `Body`. So any workflow that makes an HTTP call also needs a `FileSystemClaim` provider; tests mock its `write` to return a `Binary`.
|
|
86
85
|
|
|
87
86
|
### CacheClaim (OAuth only)
|
|
88
87
|
|
|
@@ -117,74 +116,21 @@ Invalid body shape throws `Invalid body`.
|
|
|
117
116
|
|
|
118
117
|
---
|
|
119
118
|
|
|
120
|
-
##
|
|
121
|
-
|
|
122
|
-
### HttpRequestActivity
|
|
123
|
-
|
|
124
|
-
Direct use when workflow input matches HTTP config:
|
|
125
|
-
|
|
126
|
-
```typescript
|
|
127
|
-
import { HttpRequestActivity } from '@t4h.framework/http'
|
|
128
|
-
|
|
129
|
-
// Inside Claim.run or reconciler context:
|
|
130
|
-
const activity = new HttpRequestActivity()
|
|
131
|
-
const output = await activity.run({
|
|
132
|
-
url: 'https://api.example.com/data',
|
|
133
|
-
method: 'GET',
|
|
134
|
-
})
|
|
135
|
-
// Serializable output is HttpRequestActivityOutput; toOutput wraps Response
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
`toInput` / `toHttpInput` pass input through; `toOutput` returns `new Response(output)`.
|
|
119
|
+
## Making HTTP calls in a workflow
|
|
139
120
|
|
|
140
|
-
|
|
121
|
+
Always use the ready **`http` / `HttpClient`** helper — it is what the package
|
|
122
|
+
exports for workflows and it drives the underlying HTTP activity through the
|
|
123
|
+
reconciler for you (replay-safe). Never define, extend, or hand-instantiate an
|
|
124
|
+
activity in workflow code.
|
|
141
125
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
Implement:
|
|
145
|
-
|
|
146
|
-
- `toInput(...args)` — map workflow args → activity input
|
|
147
|
-
- `toOutput(serializable, ...args)` — map serializable result → workflow return type
|
|
148
|
-
- `protected toHttpInput(input)` — map to `HttpRequestActivityInput`
|
|
149
|
-
- `protected toHttpOutput(output)` — map `HttpRequestActivityOutput` → serializable stored in history
|
|
126
|
+
For a typed result, read the response and map with plain TypeScript:
|
|
150
127
|
|
|
151
128
|
```typescript
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
type HttpRequestActivityInput,
|
|
155
|
-
type HttpRequestActivityOutput,
|
|
156
|
-
} from '@t4h.framework/http'
|
|
157
|
-
import type { Serializable } from '@t4h.framework/core'
|
|
158
|
-
|
|
159
|
-
class FetchStatusActivity extends AbstractHttpRequestActivity<
|
|
160
|
-
[endpoint: string],
|
|
161
|
-
{ endpoint: string },
|
|
162
|
-
{ status: number },
|
|
163
|
-
{ status: number }
|
|
164
|
-
> {
|
|
165
|
-
public async toInput(endpoint: string) {
|
|
166
|
-
return { endpoint }
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
protected async toHttpInput(input: { endpoint: string }): Promise<HttpRequestActivityInput> {
|
|
170
|
-
return { url: input.endpoint, method: 'GET' }
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
protected async toHttpOutput(output: HttpRequestActivityOutput): Promise<{ status: number }> {
|
|
174
|
-
return { status: output.status }
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
public toOutput(output: { status: number }) {
|
|
178
|
-
return output
|
|
179
|
-
}
|
|
180
|
-
}
|
|
129
|
+
const res = await http.get(`${environments.SERVICE_URL}/data`)
|
|
130
|
+
const data = await res.body.json<MyType>()
|
|
181
131
|
```
|
|
182
132
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
- `AbstractHttpRequestActivity.getContent(body)`
|
|
186
|
-
- `AbstractHttpRequestActivity.append(form, input, currentKey?)`
|
|
187
|
-
- `AbstractHttpRequestActivity.getFormData(input)`
|
|
133
|
+
See **HttpClient and `http`** and **Response and Body** below.
|
|
188
134
|
|
|
189
135
|
---
|
|
190
136
|
|
|
@@ -217,7 +163,7 @@ const client = new HttpClient({
|
|
|
217
163
|
|
|
218
164
|
Methods: `request(url, config?)`, `get`, `post(url, body, config?)`, `put`, `patch`, `delete`. Per-request `auth` in config merges over client `config.auth`.
|
|
219
165
|
|
|
220
|
-
Workflow example
|
|
166
|
+
Workflow example:
|
|
221
167
|
|
|
222
168
|
```typescript
|
|
223
169
|
import { Workflow } from '@t4h.framework/core'
|
|
@@ -254,6 +200,13 @@ await response.body.stream()
|
|
|
254
200
|
|
|
255
201
|
`body.json()` throws `Invalid content type` for non-JSON types (e.g. `text/plain`, `text/html`).
|
|
256
202
|
|
|
203
|
+
`Body` extends `SerializableClass` (`packages/http/src/models/Body.ts`) and is
|
|
204
|
+
persisted through `FileSystemClaim`, so the `Response`/`Body` returned by the
|
|
205
|
+
`http` client **is replay-safe** — it is serialized between reconciler steps and
|
|
206
|
+
rebuilt on replay. You do **not** need a custom activity to "flatten" the body
|
|
207
|
+
into history; read it with `await res.body.json<T>()` in the workflow and
|
|
208
|
+
transform with plain TypeScript.
|
|
209
|
+
|
|
257
210
|
Serializable shape: `{ binary: ... }` via `toSerializable` / `fromSerializable`.
|
|
258
211
|
|
|
259
212
|
---
|
|
@@ -308,95 +261,75 @@ Requires `CacheClaim` + `HttpClientRequestClaim` in the runtime.
|
|
|
308
261
|
|
|
309
262
|
## Testing
|
|
310
263
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
264
|
+
Test the **workflow** with `TestWorkflowEnvironment` from
|
|
265
|
+
`@t4h.framework/core/testing`, mocking the two claims the HTTP activity uses:
|
|
266
|
+
`HttpClientRequestClaim` (the network request) and `FileSystemClaim` (where the
|
|
267
|
+
activity persists the response body). See the **framework-workflow-testing** skill
|
|
268
|
+
for the full harness API.
|
|
316
269
|
|
|
317
270
|
```typescript
|
|
318
|
-
import { Claim } from '@t4h.framework/core'
|
|
319
|
-
import { FileSystemClaim } from '@t4h.framework/fs'
|
|
320
271
|
import { Readable } from 'node:stream'
|
|
272
|
+
import { Binary } from '@t4h.framework/core'
|
|
273
|
+
import { TestWorkflowEnvironment, mockClaim } from '@t4h.framework/core/testing'
|
|
321
274
|
import { HttpClientRequestClaim } from '@t4h.framework/http'
|
|
275
|
+
import { FileSystemClaim } from '@t4h.framework/fs'
|
|
322
276
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
})
|
|
328
|
-
|
|
329
|
-
const mockWrite = vi.fn().mockImplementation(async (_path, readable) => {
|
|
330
|
-
const chunks = await readable.toArray()
|
|
331
|
-
return new MockBinary(Buffer.concat(chunks), 'application/json')
|
|
332
|
-
})
|
|
333
|
-
|
|
334
|
-
const result = await Claim.run(
|
|
335
|
-
[
|
|
336
|
-
{ provide: HttpClientRequestClaim, value: { request: mockRequest } },
|
|
337
|
-
{ provide: FileSystemClaim, value: { write: mockWrite, read: vi.fn() } },
|
|
338
|
-
],
|
|
339
|
-
() => new MyActivity().run(input),
|
|
340
|
-
)
|
|
341
|
-
|
|
342
|
-
await expect(result.body.json()).resolves.toEqual({ ok: true })
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
Assert `mockRequest` received `url`, `method`, merged headers, and JSON buffer for `{ json: ... }` bodies.
|
|
346
|
-
|
|
347
|
-
### 2. Assert HttpClient schedules HttpRequestActivity
|
|
277
|
+
// Minimal Binary the FileSystemClaim mock returns from write().
|
|
278
|
+
class MockBinary extends Binary {
|
|
279
|
+
public readonly contentType = 'application/json'
|
|
280
|
+
public readonly contentLength: number
|
|
348
281
|
|
|
349
|
-
|
|
282
|
+
constructor(private readonly data: Buffer) {
|
|
283
|
+
super()
|
|
284
|
+
this.contentLength = data.byteLength
|
|
285
|
+
}
|
|
350
286
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
import { HttpRequestActivity } from '@t4h.framework/http'
|
|
354
|
-
|
|
355
|
-
async function captureTrigger(fn: () => unknown) {
|
|
356
|
-
try {
|
|
357
|
-
await History.run([], fn)
|
|
358
|
-
} catch (error) {
|
|
359
|
-
if (error instanceof PushActivity) return error
|
|
360
|
-
throw error
|
|
287
|
+
public stream() {
|
|
288
|
+
return Readable.from(this.data)
|
|
361
289
|
}
|
|
362
290
|
}
|
|
363
291
|
|
|
364
|
-
const
|
|
365
|
-
expect(trigger.activity.constructor).toBe(HttpRequestActivity)
|
|
366
|
-
expect(trigger.input).toMatchObject({ method: 'POST', body: { json: { name: 'Tony' } } })
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
### 3. Integration-test workflows with mocked claims
|
|
292
|
+
const body = Buffer.from(JSON.stringify({ ok: true }))
|
|
370
293
|
|
|
371
|
-
|
|
294
|
+
// mockImplementation (not mockResolvedValue): replay runs the workflow twice, so
|
|
295
|
+
// each call must hand back a fresh, unconsumed stream.
|
|
296
|
+
const mockRequest = vi.fn().mockImplementation(async () => ({
|
|
297
|
+
status: 200,
|
|
298
|
+
headers: { 'content-type': 'application/json' },
|
|
299
|
+
body: Readable.from(body),
|
|
300
|
+
}))
|
|
372
301
|
|
|
373
|
-
|
|
374
|
-
import { WorkflowTesting } from '@t4h.framework/core/testing'
|
|
375
|
-
import { HttpClientRequestClaim, type HttpClientRequestClaimInput } from '@t4h.framework/http'
|
|
302
|
+
const mockWrite = vi.fn().mockImplementation(async () => new MockBinary(body))
|
|
376
303
|
|
|
377
|
-
const
|
|
304
|
+
const env = TestWorkflowEnvironment.create({ now: 0 })
|
|
378
305
|
|
|
379
|
-
await
|
|
306
|
+
const output = await env.execute(workflow, input, {
|
|
380
307
|
claims: [
|
|
381
|
-
|
|
382
|
-
{
|
|
308
|
+
mockClaim(HttpClientRequestClaim, { request: mockRequest }),
|
|
309
|
+
mockClaim(FileSystemClaim, { write: mockWrite, read: vi.fn() }),
|
|
383
310
|
],
|
|
311
|
+
env: { SERVICE_URL: 'https://api.example.com' },
|
|
384
312
|
})
|
|
313
|
+
|
|
314
|
+
// request input carries a URL object
|
|
315
|
+
expect(mockRequest.mock.calls[0][0].url).toEqual(
|
|
316
|
+
new URL('https://api.example.com/data'),
|
|
317
|
+
)
|
|
385
318
|
```
|
|
386
319
|
|
|
387
|
-
Inspect `mockRequest.mock.calls` for URLs, methods, and bodies.
|
|
320
|
+
Inspect `mockRequest.mock.calls` for URLs, methods, and bodies.
|
|
388
321
|
|
|
389
322
|
---
|
|
390
323
|
|
|
391
324
|
## Checklist for agents
|
|
392
325
|
|
|
393
|
-
1.
|
|
326
|
+
1. In workflows, **always** use **`http` / `HttpClient`** — never define or extend an activity. Get typed, replay-safe results with `await res.body.json<T>()` plus plain TypeScript.
|
|
394
327
|
2. Never call `fetch` directly in workflow code if the step must be replayable — use the package APIs.
|
|
395
328
|
3. Ensure runtime provides **`HttpClientRequestClaim`** and **`FileSystemClaim`** (or pass mocks in tests).
|
|
396
329
|
4. Use **`{ json: ... }`**, **`{ text: ... }`**, or **`{ form: ... }`** for bodies — not raw `Buffer` on activity input.
|
|
397
330
|
5. Read responses with **`await response.body.json()`** or **`text()`**; check `status` explicitly (no automatic throw on 4xx/5xx).
|
|
398
331
|
6. For OAuth, wire **`CacheClaim`** and use exported grant classes only.
|
|
399
|
-
7.
|
|
332
|
+
7. Test workflows with **`TestWorkflowEnvironment`** + **`mockClaim`**, mocking **`HttpClientRequestClaim`** and **`FileSystemClaim`**; assert on `mockRequest.mock.calls`.
|
|
400
333
|
8. Do not document or import APIs not exported from `@t4h.framework/http`.
|
|
401
334
|
|
|
402
|
-
See also: `packages/http/README.md
|
|
335
|
+
See also: `packages/http/README.md`; **framework-workflow-testing** for the test harness.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@t4h.framework/http",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "HTTP module for the T4H Framework",
|
|
5
5
|
"homepage": "https://github.com/tech4humans-brasil/framework/tree/main/packages/http",
|
|
6
6
|
"bugs": "https://github.com/tech4humans-brasil/framework/issues",
|
|
@@ -38,18 +38,18 @@
|
|
|
38
38
|
"uuidv7": "^1.1.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@t4h.framework/cache": "^0.
|
|
42
|
-
"@t4h.framework/core": "^0.
|
|
43
|
-
"@t4h.framework/fs": "^0.
|
|
41
|
+
"@t4h.framework/cache": "^0.5.0",
|
|
42
|
+
"@t4h.framework/core": "^0.8.0",
|
|
43
|
+
"@t4h.framework/fs": "^0.5.0",
|
|
44
44
|
"@types/type-is": "^1.6.7",
|
|
45
45
|
"@vitest/coverage-v8": "^4.0.18",
|
|
46
46
|
"typescript": "^5.9.3",
|
|
47
47
|
"vitest": "^4.0.18"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@t4h.framework/cache": "^0.
|
|
51
|
-
"@t4h.framework/core": "^0.
|
|
52
|
-
"@t4h.framework/fs": "^0.
|
|
50
|
+
"@t4h.framework/cache": "^0.5.0",
|
|
51
|
+
"@t4h.framework/core": "^0.8.0",
|
|
52
|
+
"@t4h.framework/fs": "^0.5.0"
|
|
53
53
|
},
|
|
54
54
|
"packageManager": "yarn@4.12.0",
|
|
55
55
|
"engines": {
|