@alt-javascript/camel-lite-component-http 1.0.2

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/README.md ADDED
@@ -0,0 +1,54 @@
1
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
2
+
3
+ ## What
4
+
5
+ HTTP producer for outbound fetch requests. Sends the exchange body as the request body and sets the response body on the exchange. Producer-only — there is no embedded server/consumer. For inbound HTTP, use [Hono](https://hono.dev/) or Express and bridge via `direct:`.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install camel-lite-component-http
11
+ ```
12
+
13
+ ## URI Syntax
14
+
15
+ ```
16
+ http://host/path[?method=GET&contentType=application/json&headers.X-Custom=value]
17
+ ```
18
+
19
+ | Parameter | Default | Description |
20
+ |----------------|----------------------|-------------|
21
+ | `method` | `GET` | HTTP method: `GET`, `POST`, `PUT`, `DELETE`, or `PATCH`. |
22
+ | `contentType` | `application/json` | Value for the `Content-Type` request header. |
23
+ | `headers.*` | *(none)* | Additional request headers. e.g. `headers.Authorization=Bearer+token`. |
24
+
25
+ ## Usage
26
+
27
+ ```js
28
+ import { CamelContext } from 'camel-lite-core';
29
+ import { HttpComponent } from 'camel-lite-component-http';
30
+
31
+ const context = new CamelContext();
32
+ context.addComponent('http', new HttpComponent());
33
+
34
+ context.addRoutes({
35
+ configure(ctx) {
36
+ ctx.from('direct:callApi')
37
+ .to('http://api.example.com/users?method=POST&contentType=application/json');
38
+ }
39
+ });
40
+
41
+ await context.start();
42
+
43
+ const template = context.createProducerTemplate();
44
+ const exchange = await template.send('direct:callApi', ex => {
45
+ ex.in.body = JSON.stringify({ name: 'Alice' });
46
+ });
47
+ console.log('Response:', exchange.in.body);
48
+
49
+ await context.stop();
50
+ ```
51
+
52
+ ## See Also
53
+
54
+ [camel-lite — root README](../../README.md)
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@alt-javascript/camel-lite-component-http",
3
+ "version": "1.0.2",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./src/index.js"
7
+ },
8
+ "dependencies": {
9
+ "@alt-javascript/logger": "^3.0.7",
10
+ "@alt-javascript/config": "^3.0.7",
11
+ "@alt-javascript/common": "^3.0.7",
12
+ "@alt-javascript/camel-lite-core": "1.0.2"
13
+ },
14
+ "scripts": {
15
+ "test": "node --test"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/alt-javascript/camel-lite"
20
+ },
21
+ "author": "Craig Parravicini",
22
+ "contributors": [
23
+ "Claude (Anthropic)",
24
+ "Apache Camel — design inspiration and pattern source"
25
+ ],
26
+ "keywords": [
27
+ "alt-javascript",
28
+ "camel",
29
+ "camel-lite",
30
+ "eai",
31
+ "eip",
32
+ "integration",
33
+ "http",
34
+ "https",
35
+ "rest",
36
+ "component"
37
+ ],
38
+ "publishConfig": {
39
+ "registry": "https://registry.npmjs.org/",
40
+ "access": "public"
41
+ }
42
+ }
@@ -0,0 +1,11 @@
1
+ import { Component } from '@alt-javascript/camel-lite-core';
2
+ import HttpEndpoint from './HttpEndpoint.js';
3
+
4
+ class HttpComponent extends Component {
5
+ createEndpoint(uri, remaining, parameters, context) {
6
+ return new HttpEndpoint(uri, remaining, parameters, context);
7
+ }
8
+ }
9
+
10
+ export { HttpComponent };
11
+ export default HttpComponent;
@@ -0,0 +1,55 @@
1
+ import { Endpoint } from '@alt-javascript/camel-lite-core';
2
+ import HttpProducer from './HttpProducer.js';
3
+
4
+ class HttpEndpoint extends Endpoint {
5
+ #uri;
6
+ #url;
7
+ #method;
8
+
9
+ constructor(uri, remaining, parameters, context) {
10
+ super();
11
+ this.#uri = uri;
12
+
13
+ const params = parameters instanceof URLSearchParams
14
+ ? parameters
15
+ : new URLSearchParams(typeof parameters === 'string' ? parameters : '');
16
+
17
+ this.#method = (params.get('method') ?? 'GET').toUpperCase();
18
+
19
+ // Reconstruct the full URL from the original URI:
20
+ // 'http:example.com/path?method=GET' → 'http://example.com/path'
21
+ // Strip the camel-lite scheme prefix and any component-level query params.
22
+ // The URI already contains the real scheme (http/https) so we just fix the
23
+ // double-colon: 'http:host/path' → 'http://host/path'
24
+ const colonIdx = uri.indexOf(':');
25
+ const scheme = colonIdx >= 0 ? uri.slice(0, colonIdx) : 'http';
26
+ const afterScheme = colonIdx >= 0 ? uri.slice(colonIdx + 1) : uri;
27
+
28
+ // Strip leading slashes that aren't part of the authority (Camel URIs use single colon)
29
+ // then reconstruct proper URL
30
+ const withoutLeadingSlashes = afterScheme.replace(/^\/\//, '');
31
+
32
+ // Strip component-level query params (method=, etc.) — keep only real URL query params
33
+ // by rebuilding: take the raw remaining which has path+query, but strip known params
34
+ const realParams = new URLSearchParams(params);
35
+ realParams.delete('method');
36
+ const queryStr = realParams.toString();
37
+ const pathWithoutQuery = remaining.split('?')[0];
38
+ this.#url = `${scheme}://${pathWithoutQuery}${queryStr ? '?' + queryStr : ''}`;
39
+ }
40
+
41
+ get uri() { return this.#uri; }
42
+ get url() { return this.#url; }
43
+ get method() { return this.#method; }
44
+
45
+ createProducer() {
46
+ return new HttpProducer(this.#url, this.#method);
47
+ }
48
+
49
+ createConsumer() {
50
+ throw new Error('http: component is producer-only (see D005)');
51
+ }
52
+ }
53
+
54
+ export { HttpEndpoint };
55
+ export default HttpEndpoint;
@@ -0,0 +1,56 @@
1
+ import { Producer } from '@alt-javascript/camel-lite-core';
2
+ import { LoggerFactory } from '@alt-javascript/logger';
3
+
4
+ const log = LoggerFactory.getLogger('@alt-javascript/camel-lite/HttpProducer');
5
+
6
+ const BODY_METHODS = new Set(['POST', 'PUT', 'PATCH']);
7
+
8
+ class HttpProducer extends Producer {
9
+ #url;
10
+ #method;
11
+
12
+ constructor(url, method = 'GET') {
13
+ super();
14
+ this.#url = url;
15
+ this.#method = method.toUpperCase();
16
+ }
17
+
18
+ get url() { return this.#url; }
19
+ get method() { return this.#method; }
20
+
21
+ async send(exchange) {
22
+ // Method: CamelHttpMethod header > constructor default
23
+ const method = (exchange.in.getHeader('CamelHttpMethod') ?? this.#method).toUpperCase();
24
+
25
+ // Optional override URL from header
26
+ const url = exchange.in.getHeader('CamelHttpUri') ?? this.#url;
27
+
28
+ // Build request body for POST/PUT/PATCH
29
+ let body = undefined;
30
+ if (BODY_METHODS.has(method) && exchange.in.body != null) {
31
+ body = typeof exchange.in.body === 'string'
32
+ ? exchange.in.body
33
+ : JSON.stringify(exchange.in.body);
34
+ }
35
+
36
+ log.debug(`HTTP ${method} ${url}`);
37
+
38
+ const response = await fetch(url, { method, body });
39
+
40
+ log.info(`HTTP ${response.status} ${url}`);
41
+
42
+ const responseBody = await response.text();
43
+
44
+ exchange.out.body = responseBody;
45
+ exchange.out.setHeader('CamelHttpResponseCode', response.status);
46
+ exchange.out.setHeader('CamelHttpResponseText', response.statusText);
47
+
48
+ // Copy all response headers
49
+ for (const [key, value] of response.headers) {
50
+ exchange.out.setHeader(key, value);
51
+ }
52
+ }
53
+ }
54
+
55
+ export { HttpProducer };
56
+ export default HttpProducer;
package/src/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { HttpComponent } from './HttpComponent.js';
2
+ export { HttpEndpoint } from './HttpEndpoint.js';
3
+ export { HttpProducer } from './HttpProducer.js';
4
+
5
+ import HttpComponent from './HttpComponent.js';
6
+ export default HttpComponent;
@@ -0,0 +1,172 @@
1
+ import { describe, it, before, after } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { createServer } from 'node:http';
4
+ import { CamelContext, Exchange, Component } from '@alt-javascript/camel-lite-core';
5
+ import { HttpComponent, HttpEndpoint, HttpProducer } from '@alt-javascript/camel-lite-component-http';
6
+
7
+ // Spin a minimal local HTTP server for testing — no external network calls
8
+ function makeTestServer(handler) {
9
+ return new Promise((resolve) => {
10
+ const server = createServer(handler);
11
+ server.listen(0, '127.0.0.1', () => {
12
+ const { port } = server.address();
13
+ resolve({ server, port });
14
+ });
15
+ });
16
+ }
17
+
18
+ describe('HttpProducer', () => {
19
+ it('GET request: exchange.out.body contains response and CamelHttpResponseCode is 200', async () => {
20
+ const { server, port } = await makeTestServer((req, res) => {
21
+ res.writeHead(200, { 'content-type': 'application/json' });
22
+ res.end(JSON.stringify({ ok: true, method: req.method }));
23
+ });
24
+
25
+ try {
26
+ const producer = new HttpProducer(`http://127.0.0.1:${port}/test`);
27
+ const exchange = new Exchange();
28
+ await producer.send(exchange);
29
+
30
+ assert.equal(exchange.out.getHeader('CamelHttpResponseCode'), 200);
31
+ const body = JSON.parse(exchange.out.body);
32
+ assert.equal(body.ok, true);
33
+ assert.equal(body.method, 'GET');
34
+ assert.equal(exchange.exception, null);
35
+ } finally {
36
+ await new Promise(r => server.close(r));
37
+ }
38
+ });
39
+
40
+ it('POST request sends body and receives echo', async () => {
41
+ const { server, port } = await makeTestServer((req, res) => {
42
+ let data = '';
43
+ req.on('data', c => { data += c; });
44
+ req.on('end', () => {
45
+ res.writeHead(200, { 'content-type': 'text/plain' });
46
+ res.end('echo:' + data);
47
+ });
48
+ });
49
+
50
+ try {
51
+ const producer = new HttpProducer(`http://127.0.0.1:${port}/echo`, 'POST');
52
+ const exchange = new Exchange();
53
+ exchange.in.body = 'ping';
54
+ await producer.send(exchange);
55
+
56
+ assert.equal(exchange.out.getHeader('CamelHttpResponseCode'), 200);
57
+ assert.equal(exchange.out.body, 'echo:ping');
58
+ } finally {
59
+ await new Promise(r => server.close(r));
60
+ }
61
+ });
62
+
63
+ it('CamelHttpMethod header overrides default method', async () => {
64
+ const methods = [];
65
+ const { server, port } = await makeTestServer((req, res) => {
66
+ methods.push(req.method);
67
+ res.writeHead(200);
68
+ res.end('ok');
69
+ });
70
+
71
+ try {
72
+ // Default is GET, but header overrides to PUT
73
+ const producer = new HttpProducer(`http://127.0.0.1:${port}/`);
74
+ const exchange = new Exchange();
75
+ exchange.in.setHeader('CamelHttpMethod', 'PUT');
76
+ exchange.in.body = 'data';
77
+ await producer.send(exchange);
78
+
79
+ assert.equal(methods[0], 'PUT');
80
+ } finally {
81
+ await new Promise(r => server.close(r));
82
+ }
83
+ });
84
+
85
+ it('404 response: CamelHttpResponseCode is 404, exchange.exception is null', async () => {
86
+ const { server, port } = await makeTestServer((req, res) => {
87
+ res.writeHead(404);
88
+ res.end('not found');
89
+ });
90
+
91
+ try {
92
+ const producer = new HttpProducer(`http://127.0.0.1:${port}/missing`);
93
+ const exchange = new Exchange();
94
+ await producer.send(exchange);
95
+
96
+ // HTTP errors are not exceptions — they land in exchange.out
97
+ assert.equal(exchange.out.getHeader('CamelHttpResponseCode'), 404);
98
+ assert.equal(exchange.out.body, 'not found');
99
+ assert.equal(exchange.exception, null);
100
+ } finally {
101
+ await new Promise(r => server.close(r));
102
+ }
103
+ });
104
+
105
+ it('POST with object body: serialised as JSON', async () => {
106
+ let receivedBody = '';
107
+ const { server, port } = await makeTestServer((req, res) => {
108
+ let data = '';
109
+ req.on('data', c => { data += c; });
110
+ req.on('end', () => {
111
+ receivedBody = data;
112
+ res.writeHead(200);
113
+ res.end('ok');
114
+ });
115
+ });
116
+
117
+ try {
118
+ const producer = new HttpProducer(`http://127.0.0.1:${port}/`, 'POST');
119
+ const exchange = new Exchange();
120
+ exchange.in.body = { hello: 'world' };
121
+ await producer.send(exchange);
122
+
123
+ assert.deepEqual(JSON.parse(receivedBody), { hello: 'world' });
124
+ } finally {
125
+ await new Promise(r => server.close(r));
126
+ }
127
+ });
128
+ });
129
+
130
+ describe('HttpEndpoint', () => {
131
+ it('reconstructs URL correctly from URI', () => {
132
+ const ctx = new CamelContext();
133
+ const ep = new HttpEndpoint(
134
+ 'http:example.com/api/v1',
135
+ 'example.com/api/v1',
136
+ new URLSearchParams(),
137
+ ctx
138
+ );
139
+ assert.equal(ep.url, 'http://example.com/api/v1');
140
+ assert.equal(ep.method, 'GET');
141
+ });
142
+
143
+ it('method= URI param sets default method', () => {
144
+ const ctx = new CamelContext();
145
+ const ep = new HttpEndpoint(
146
+ 'http:example.com/api?method=POST',
147
+ 'example.com/api',
148
+ new URLSearchParams('method=POST'),
149
+ ctx
150
+ );
151
+ assert.equal(ep.method, 'POST');
152
+ });
153
+
154
+ it('createConsumer throws (producer-only)', () => {
155
+ const ctx = new CamelContext();
156
+ const ep = new HttpEndpoint('http:example.com', 'example.com', new URLSearchParams(), ctx);
157
+ assert.throws(() => ep.createConsumer(), { message: /producer-only/ });
158
+ });
159
+ });
160
+
161
+ describe('cross-package import integration', () => {
162
+ it('HttpComponent is a subclass of Component', () => {
163
+ assert.ok(new HttpComponent() instanceof Component);
164
+ });
165
+
166
+ it('HttpComponent.createEndpoint returns HttpEndpoint', () => {
167
+ const ctx = new CamelContext();
168
+ const comp = new HttpComponent();
169
+ const ep = comp.createEndpoint('http:example.com', 'example.com', new URLSearchParams(), ctx);
170
+ assert.ok(ep instanceof HttpEndpoint);
171
+ });
172
+ });