@codemation/core-nodes 0.6.0 → 0.7.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/CHANGELOG.md +22 -0
- package/dist/index.cjs +76 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +80 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.js +76 -16
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/http/HttpBodyBuilder.ts +45 -17
- package/src/http/HttpRequestExecutor.ts +4 -1
- package/src/http/httpRequest.types.ts +27 -0
- package/src/nodes/HttpRequestNodeFactory.ts +69 -1
- package/src/nodes/httpRequest.ts +44 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @codemation/core-nodes
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#123](https://github.com/MadeRelevant/codemation/pull/123) [`c191557`](https://github.com/MadeRelevant/codemation/commit/c19155783a012d293568f55427ae36b31171af11) Thanks [@cblokland90](https://github.com/cblokland90)! - feat(core-nodes): HttpRequest body and response support binary slots
|
|
8
|
+
- Add `responseFormat: "binary"` config field to store response bytes directly in `ctx.binary` rather than parsing as JSON/text. Output JSON carries `{ status, headers, binarySlot, contentType, size, filename }`.
|
|
9
|
+
- Add `responseBinarySlot?: string` (default `"response"`) and `responseSizeCapBytes?: number` (default 100 MiB, checked against `Content-Length` before allocating).
|
|
10
|
+
- Add `body: { kind: "binary", slot: string }` body spec to send raw bytes from a binary attachment slot as the request body. The attachment's `mimeType` is used as `Content-Type` unless an explicit header overrides it.
|
|
11
|
+
- Fix: explicit `headers["content-type"]` now correctly wins over the body-derived content type for all body kinds (was previously overwritten).
|
|
12
|
+
- Extract `HttpBodyBuilder.readStreamToBuffer` private helper to deduplicate stream-reading code shared between multipart and binary body kinds.
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- [#126](https://github.com/MadeRelevant/codemation/pull/126) [`d0f2bd9`](https://github.com/MadeRelevant/codemation/commit/d0f2bd9a670ff80c2e2e12f7c410c63d14c94b55) Thanks [@cblokland90](https://github.com/cblokland90)! - DriveDownload and OnNewMail now stream binary attachments directly into binary storage instead of buffering the entire payload in RAM (`Buffer.concat` / `Buffer.from(x, "base64")`). Functionally equivalent — only the memory profile improves (critical for multi-GB files).
|
|
17
|
+
|
|
18
|
+
Adds `codemation/no-buffer-everything` ESLint rule (error severity) to prevent future regressions: flags `Buffer.from(x,"base64")`, `.arrayBuffer()`, and `Buffer.concat()` with guidance on streaming alternatives. Genuine constraints (AES-GCM cipher, Graph upload requiring Content-Length, Excel workbook responses) are suppressed with justified `-- <reason>` comments.
|
|
19
|
+
|
|
20
|
+
Follow-up: support streaming multipart upload via the form-data package to remove the suppression in `HttpBodyBuilder`.
|
|
21
|
+
|
|
22
|
+
- Updated dependencies [[`1f10121`](https://github.com/MadeRelevant/codemation/commit/1f10121a093ef0612a33c873419b032709c9964d)]:
|
|
23
|
+
- @codemation/core@0.10.1
|
|
24
|
+
|
|
3
25
|
## 0.6.0
|
|
4
26
|
|
|
5
27
|
### Minor Changes
|
package/dist/index.cjs
CHANGED
|
@@ -323,7 +323,8 @@ var HttpRequestExecutor = class {
|
|
|
323
323
|
...credentialDelta.query ?? {}
|
|
324
324
|
};
|
|
325
325
|
const encodedBody = await this.bodyBuilder.build(spec.body, item, spec.ctx);
|
|
326
|
-
|
|
326
|
+
const hasExplicitContentType = Object.keys(mergedHeaders).some((k) => k.toLowerCase() === "content-type");
|
|
327
|
+
if (encodedBody && encodedBody.contentType && !hasExplicitContentType) mergedHeaders["content-type"] = encodedBody.contentType;
|
|
327
328
|
return {
|
|
328
329
|
url: this.urlBuilder.build(spec.url, mergedQuery),
|
|
329
330
|
init: {
|
|
@@ -422,21 +423,7 @@ var HttpBodyBuilder = class {
|
|
|
422
423
|
if (attachment) {
|
|
423
424
|
const readResult = await ctx.binary.openReadStream(attachment);
|
|
424
425
|
if (readResult) {
|
|
425
|
-
const
|
|
426
|
-
const chunks = [];
|
|
427
|
-
let done = false;
|
|
428
|
-
while (!done) {
|
|
429
|
-
const result = await reader.read();
|
|
430
|
-
done = result.done;
|
|
431
|
-
if (result.value) chunks.push(result.value);
|
|
432
|
-
}
|
|
433
|
-
const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
|
|
434
|
-
const merged = new Uint8Array(totalLength);
|
|
435
|
-
let offset = 0;
|
|
436
|
-
for (const chunk of chunks) {
|
|
437
|
-
merged.set(chunk, offset);
|
|
438
|
-
offset += chunk.length;
|
|
439
|
-
}
|
|
426
|
+
const merged = await this.readStreamToBuffer(readResult.body);
|
|
440
427
|
const blob = new Blob([merged], { type: attachment.mimeType });
|
|
441
428
|
formData.append(fieldName, blob, attachment.filename ?? binaryRef);
|
|
442
429
|
}
|
|
@@ -447,6 +434,34 @@ var HttpBodyBuilder = class {
|
|
|
447
434
|
contentType: ""
|
|
448
435
|
};
|
|
449
436
|
}
|
|
437
|
+
if (spec.kind === "binary") {
|
|
438
|
+
const attachment = item.binary?.[spec.slot];
|
|
439
|
+
if (!attachment) throw new Error(`HttpRequest bodyFormat "binary": no binary attachment found at slot "${spec.slot}". Ensure a previous node attached binary data at that slot.`);
|
|
440
|
+
const readResult = await ctx.binary.openReadStream(attachment);
|
|
441
|
+
if (!readResult) throw new Error(`HttpRequest bodyFormat "binary": could not open read stream for slot "${spec.slot}".`);
|
|
442
|
+
return {
|
|
443
|
+
body: readResult.body,
|
|
444
|
+
contentType: attachment.mimeType
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
async readStreamToBuffer(stream) {
|
|
449
|
+
const reader = stream.getReader();
|
|
450
|
+
const chunks = [];
|
|
451
|
+
let done = false;
|
|
452
|
+
while (!done) {
|
|
453
|
+
const result = await reader.read();
|
|
454
|
+
done = result.done;
|
|
455
|
+
if (result.value) chunks.push(result.value);
|
|
456
|
+
}
|
|
457
|
+
const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
|
|
458
|
+
const merged = new Uint8Array(new ArrayBuffer(totalLength));
|
|
459
|
+
let offset = 0;
|
|
460
|
+
for (const chunk of chunks) {
|
|
461
|
+
merged.set(chunk, offset);
|
|
462
|
+
offset += chunk.length;
|
|
463
|
+
}
|
|
464
|
+
return merged;
|
|
450
465
|
}
|
|
451
466
|
};
|
|
452
467
|
|
|
@@ -6540,12 +6555,16 @@ let HttpRequestNode = class HttpRequestNode$1 {
|
|
|
6540
6555
|
mode: ctx.config.downloadMode,
|
|
6541
6556
|
binaryName: ctx.config.binaryName
|
|
6542
6557
|
},
|
|
6558
|
+
responseFormat: ctx.config.responseFormat,
|
|
6559
|
+
responseBinarySlot: ctx.config.responseBinarySlot,
|
|
6560
|
+
responseSizeCapBytes: ctx.config.responseSizeCapBytes,
|
|
6543
6561
|
ctx
|
|
6544
6562
|
};
|
|
6545
6563
|
const { url: resolvedUrl, init } = await new HttpRequestExecutor(globalThis.fetch, new HttpBodyBuilder(), new HttpUrlBuilder()).buildRequest(spec, item);
|
|
6546
6564
|
const response = await globalThis.fetch(resolvedUrl, init);
|
|
6547
6565
|
const headers = this.readHeaders(response.headers);
|
|
6548
6566
|
const mimeType = this.resolveMimeType(headers);
|
|
6567
|
+
if (ctx.config.responseFormat === "binary") return await this.handleBinaryResponse(response, resolvedUrl, headers, mimeType, ctx);
|
|
6549
6568
|
const binaryName = ctx.config.binaryName;
|
|
6550
6569
|
if (this.shouldAttachBody(ctx.config.downloadMode, mimeType)) {
|
|
6551
6570
|
const outputJson = {
|
|
@@ -6589,6 +6608,36 @@ let HttpRequestNode = class HttpRequestNode$1 {
|
|
|
6589
6608
|
...text !== void 0 ? { text } : {}
|
|
6590
6609
|
} };
|
|
6591
6610
|
}
|
|
6611
|
+
async handleBinaryResponse(response, resolvedUrl, headers, mimeType, ctx) {
|
|
6612
|
+
const slotName = ctx.config.responseBinarySlot;
|
|
6613
|
+
const sizeCap = ctx.config.responseSizeCapBytes;
|
|
6614
|
+
const contentLengthHeader = headers["content-length"];
|
|
6615
|
+
if (contentLengthHeader) {
|
|
6616
|
+
const declaredSize = parseInt(contentLengthHeader, 10);
|
|
6617
|
+
if (!isNaN(declaredSize) && declaredSize > sizeCap) throw new Error(`HttpRequest responseFormat "binary": response Content-Length (${declaredSize} bytes) exceeds responseSizeCapBytes (${sizeCap} bytes).`);
|
|
6618
|
+
}
|
|
6619
|
+
const filename = this.resolveFilename(resolvedUrl, headers);
|
|
6620
|
+
const attachment = await ctx.binary.attach({
|
|
6621
|
+
name: slotName,
|
|
6622
|
+
body: response.body ? response.body : new Uint8Array(await response.arrayBuffer()),
|
|
6623
|
+
mimeType,
|
|
6624
|
+
filename
|
|
6625
|
+
});
|
|
6626
|
+
let outputItem = { json: {
|
|
6627
|
+
url: resolvedUrl,
|
|
6628
|
+
method: ctx.config.method,
|
|
6629
|
+
ok: response.ok,
|
|
6630
|
+
status: response.status,
|
|
6631
|
+
statusText: response.statusText,
|
|
6632
|
+
headers,
|
|
6633
|
+
binarySlot: slotName,
|
|
6634
|
+
contentType: mimeType,
|
|
6635
|
+
size: attachment.size,
|
|
6636
|
+
...filename !== void 0 ? { filename } : {}
|
|
6637
|
+
} };
|
|
6638
|
+
outputItem = ctx.binary.withAttachment(outputItem, slotName, attachment);
|
|
6639
|
+
return outputItem;
|
|
6640
|
+
}
|
|
6592
6641
|
async resolveCredential(ctx) {
|
|
6593
6642
|
const slotKey = ctx.config.args.credentialSlot;
|
|
6594
6643
|
if (!slotKey) return;
|
|
@@ -6660,6 +6709,8 @@ const HTTP_REQUEST_ACCEPTED_CREDENTIAL_TYPES = [
|
|
|
6660
6709
|
basicAuthCredentialType.definition.typeId,
|
|
6661
6710
|
oauth2ClientCredentialsType.definition.typeId
|
|
6662
6711
|
];
|
|
6712
|
+
/** Default maximum response size for binary mode: 100 MiB. */
|
|
6713
|
+
const DEFAULT_RESPONSE_SIZE_CAP_BYTES = 100 * 1024 * 1024;
|
|
6663
6714
|
var HttpRequest = class {
|
|
6664
6715
|
kind = "node";
|
|
6665
6716
|
type = HttpRequestNode;
|
|
@@ -6685,6 +6736,15 @@ var HttpRequest = class {
|
|
|
6685
6736
|
get downloadMode() {
|
|
6686
6737
|
return this.args.downloadMode ?? "auto";
|
|
6687
6738
|
}
|
|
6739
|
+
get responseFormat() {
|
|
6740
|
+
return this.args.responseFormat;
|
|
6741
|
+
}
|
|
6742
|
+
get responseBinarySlot() {
|
|
6743
|
+
return this.args.responseBinarySlot ?? "response";
|
|
6744
|
+
}
|
|
6745
|
+
get responseSizeCapBytes() {
|
|
6746
|
+
return this.args.responseSizeCapBytes ?? DEFAULT_RESPONSE_SIZE_CAP_BYTES;
|
|
6747
|
+
}
|
|
6688
6748
|
getCredentialRequirements() {
|
|
6689
6749
|
if (!this.args.credentialSlot) return [];
|
|
6690
6750
|
return [{
|