@codemation/core-nodes 0.4.3 → 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.
Files changed (55) hide show
  1. package/CHANGELOG.md +237 -0
  2. package/dist/index.cjs +3541 -470
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +1843 -685
  5. package/dist/index.d.ts +1843 -685
  6. package/dist/index.js +3498 -465
  7. package/dist/index.js.map +1 -1
  8. package/package.json +8 -5
  9. package/src/authoring/defineRestNode.types.ts +204 -0
  10. package/src/chatModels/OpenAIChatModelFactory.ts +17 -8
  11. package/src/chatModels/OpenAiStrictJsonSchemaFactory.ts +123 -0
  12. package/src/credentials/ApiKeyCredentialType.ts +60 -0
  13. package/src/credentials/BasicAuthCredentialType.ts +51 -0
  14. package/src/credentials/BearerTokenCredentialType.ts +40 -0
  15. package/src/credentials/OAuth2ClientCredentialsTypeFactory.ts +117 -0
  16. package/src/credentials/OAuth2TokenExchangeFactory.ts +52 -0
  17. package/src/credentials/index.ts +4 -0
  18. package/src/http/HttpBodyBuilder.ts +118 -0
  19. package/src/http/HttpRequestExecutor.ts +153 -0
  20. package/src/http/HttpUrlBuilder.ts +22 -0
  21. package/src/http/httpRequest.types.ts +96 -0
  22. package/src/index.ts +10 -1
  23. package/src/nodes/AIAgentExecutionHelpersFactory.ts +45 -59
  24. package/src/nodes/AIAgentNode.ts +391 -288
  25. package/src/nodes/AgentMessageFactory.ts +57 -49
  26. package/src/nodes/AgentStructuredOutputRunner.ts +65 -71
  27. package/src/nodes/AgentToolExecutionCoordinator.ts +31 -16
  28. package/src/nodes/AssertionNode.ts +42 -0
  29. package/src/nodes/CronTriggerFactory.ts +45 -0
  30. package/src/nodes/CronTriggerNode.ts +40 -0
  31. package/src/nodes/HttpRequestNodeFactory.ts +160 -16
  32. package/src/nodes/IsTestRunNode.ts +25 -0
  33. package/src/nodes/NodeBackedToolRuntime.ts +40 -4
  34. package/src/nodes/TestTriggerNode.ts +33 -0
  35. package/src/nodes/WebhookTriggerFactory.ts +1 -1
  36. package/src/nodes/aggregate.ts +1 -1
  37. package/src/nodes/aiAgentSupport.types.ts +22 -2
  38. package/src/nodes/assertion.ts +42 -0
  39. package/src/nodes/collections/collectionDeleteNode.types.ts +23 -0
  40. package/src/nodes/collections/collectionFindOneNode.types.ts +26 -0
  41. package/src/nodes/collections/collectionGetNode.types.ts +26 -0
  42. package/src/nodes/collections/collectionInsertNode.types.ts +22 -0
  43. package/src/nodes/collections/collectionListNode.types.ts +30 -0
  44. package/src/nodes/collections/collectionUpdateNode.types.ts +23 -0
  45. package/src/nodes/collections/index.ts +6 -0
  46. package/src/nodes/httpRequest.ts +106 -1
  47. package/src/nodes/if.ts +1 -1
  48. package/src/nodes/isTestRun.ts +24 -0
  49. package/src/nodes/mapData.ts +1 -0
  50. package/src/nodes/merge.ts +1 -1
  51. package/src/nodes/noOp.ts +1 -0
  52. package/src/nodes/split.ts +1 -1
  53. package/src/nodes/testTrigger.ts +72 -0
  54. package/src/nodes/wait.ts +1 -0
  55. package/src/chatModels/OpenAIStructuredOutputMethodFactory.ts +0 -46
@@ -1,5 +1,18 @@
1
- import { RetryPolicy, type RetryPolicySpec, type RunnableNodeConfig, type TypeToken } from "@codemation/core";
1
+ import {
2
+ RetryPolicy,
3
+ type CredentialRequirement,
4
+ type RetryPolicySpec,
5
+ type RunnableNodeConfig,
6
+ type TypeToken,
7
+ } from "@codemation/core";
2
8
 
9
+ import type { HttpBodySpec } from "../http/httpRequest.types";
10
+ import {
11
+ apiKeyCredentialType,
12
+ basicAuthCredentialType,
13
+ bearerTokenCredentialType,
14
+ oauth2ClientCredentialsType,
15
+ } from "../credentials/index";
3
16
  import { HttpRequestNode } from "./HttpRequestNodeFactory";
4
17
 
5
18
  export type HttpRequestDownloadMode = "auto" | "always" | "never";
@@ -13,9 +26,33 @@ export type HttpRequestOutputJson = Readonly<{
13
26
  statusText: string;
14
27
  mimeType: string;
15
28
  headers: Readonly<Record<string, string>>;
29
+ json?: unknown;
30
+ text?: string;
16
31
  bodyBinaryName?: string;
32
+ /** Set when `responseFormat === "binary"`. Name of the binary slot the response was stored in. */
33
+ binarySlot?: string;
34
+ /** Set when `responseFormat === "binary"`. MIME type of the stored response. */
35
+ contentType?: string;
36
+ /** Set when `responseFormat === "binary"`. Size in bytes of the stored response. */
37
+ size?: number;
38
+ /** Set when `responseFormat === "binary"`. Filename inferred from URL or Content-Disposition. */
39
+ filename?: string;
17
40
  }>;
18
41
 
42
+ /**
43
+ * The built-in HTTP request credential type IDs accepted by the `HttpRequest` node.
44
+ * These match the four generic credential types shipped with `@codemation/core-nodes`.
45
+ */
46
+ export const HTTP_REQUEST_ACCEPTED_CREDENTIAL_TYPES: ReadonlyArray<string> = [
47
+ bearerTokenCredentialType.definition.typeId,
48
+ apiKeyCredentialType.definition.typeId,
49
+ basicAuthCredentialType.definition.typeId,
50
+ oauth2ClientCredentialsType.definition.typeId,
51
+ ] as const;
52
+
53
+ /** Default maximum response size for binary mode: 100 MiB. */
54
+ const DEFAULT_RESPONSE_SIZE_CAP_BYTES = 100 * 1024 * 1024;
55
+
19
56
  export class HttpRequest<
20
57
  TInputJson = Readonly<{ url?: string }>,
21
58
  TOutputJson = HttpRequestOutputJson,
@@ -23,14 +60,55 @@ export class HttpRequest<
23
60
  readonly kind = "node" as const;
24
61
  readonly type: TypeToken<unknown> = HttpRequestNode;
25
62
  readonly execution = { hint: "local" } as const;
63
+ readonly icon = "lucide:globe" as const;
26
64
 
27
65
  constructor(
28
66
  public readonly name: string,
29
67
  public readonly args: Readonly<{
68
+ /** HTTP method (default: GET). */
30
69
  method?: string;
70
+ /**
71
+ * Legacy: field name on item.json to read the URL from.
72
+ * Use `url` for a literal/templated URL instead.
73
+ */
31
74
  urlField?: string;
75
+ /** Literal or templated URL. When present, takes precedence over `urlField`. */
76
+ url?: string;
77
+ /** Extra headers to add to every request. */
78
+ headers?: Readonly<Record<string, string>>;
79
+ /** Query parameters to append to the URL. */
80
+ query?: Readonly<Record<string, string>>;
81
+ /** Request body specification. For canvas use, pass a JSON string in `body.data`. */
82
+ body?: HttpBodySpec;
83
+ /**
84
+ * Credential slot key. When set, the node resolves a credential via
85
+ * `ctx.getCredential(credentialSlot)` and applies it to the request.
86
+ * The slot must be declared in `getCredentialRequirements()`.
87
+ */
88
+ credentialSlot?: string;
32
89
  binaryName?: string;
33
90
  downloadMode?: HttpRequestDownloadMode;
91
+ /**
92
+ * Controls how the response body is handled.
93
+ * - `"json"` / `"text"`: existing behaviour (parse + emit on `item.json`).
94
+ * - `"binary"`: read the response as raw bytes and store via `ctx.binary.attach`.
95
+ * The output JSON contains `{ status, headers, binarySlot, contentType, size, filename }`
96
+ * but NOT the raw bytes. Use `responseBinarySlot` to name the slot (default `"response"`).
97
+ *
98
+ * When omitted, the existing `downloadMode` logic applies (backward-compatible).
99
+ */
100
+ responseFormat?: "json" | "text" | "binary";
101
+ /**
102
+ * Name of the binary slot to write the response body into when `responseFormat === "binary"`.
103
+ * Defaults to `"response"`.
104
+ */
105
+ responseBinarySlot?: string;
106
+ /**
107
+ * Maximum response size in bytes for binary mode. Checked against the `Content-Length`
108
+ * response header before allocating memory. Defaults to 100 MiB (104857600).
109
+ * Requests whose `Content-Length` exceeds this cap are rejected before the body is read.
110
+ */
111
+ responseSizeCapBytes?: number;
34
112
  id?: string;
35
113
  }> = {},
36
114
  public readonly retryPolicy: RetryPolicySpec = RetryPolicy.defaultForHttp,
@@ -55,6 +133,33 @@ export class HttpRequest<
55
133
  get downloadMode(): HttpRequestDownloadMode {
56
134
  return this.args.downloadMode ?? "auto";
57
135
  }
136
+
137
+ get responseFormat(): "json" | "text" | "binary" | undefined {
138
+ return this.args.responseFormat;
139
+ }
140
+
141
+ get responseBinarySlot(): string {
142
+ return this.args.responseBinarySlot ?? "response";
143
+ }
144
+
145
+ get responseSizeCapBytes(): number {
146
+ return this.args.responseSizeCapBytes ?? DEFAULT_RESPONSE_SIZE_CAP_BYTES;
147
+ }
148
+
149
+ getCredentialRequirements(): ReadonlyArray<CredentialRequirement> {
150
+ if (!this.args.credentialSlot) {
151
+ return [];
152
+ }
153
+ return [
154
+ {
155
+ slotKey: this.args.credentialSlot,
156
+ label: "Authentication",
157
+ acceptedTypes: HTTP_REQUEST_ACCEPTED_CREDENTIAL_TYPES,
158
+ optional: true,
159
+ helpText: "Optional credential for authenticating the HTTP request.",
160
+ },
161
+ ];
162
+ }
58
163
  }
59
164
 
60
165
  export { HttpRequestNode } from "./HttpRequestNodeFactory";
package/src/nodes/if.ts CHANGED
@@ -6,7 +6,7 @@ export class If<TInputJson = unknown> implements RunnableNodeConfig<TInputJson,
6
6
  readonly kind = "node" as const;
7
7
  readonly type: TypeToken<unknown> = IfNode;
8
8
  readonly execution = { hint: "local" } as const;
9
- readonly icon = "lucide:split" as const;
9
+ readonly icon = "lucide:split@rot=90" as const;
10
10
  readonly declaredOutputPorts = ["true", "false"] as const;
11
11
  constructor(
12
12
  public readonly name: string,
@@ -0,0 +1,24 @@
1
+ import type { RunnableNodeConfig, TypeToken } from "@codemation/core";
2
+
3
+ import { IsTestRunNode } from "./IsTestRunNode";
4
+
5
+ /**
6
+ * Branches per-item on whether the current run is a test run. Output ports: `true`, `false`.
7
+ * The wire payload is unchanged — this is a router, not a transform.
8
+ */
9
+ export class IsTestRun<TInputJson = unknown> implements RunnableNodeConfig<TInputJson, TInputJson> {
10
+ readonly kind = "node" as const;
11
+ readonly type: TypeToken<unknown> = IsTestRunNode;
12
+ readonly execution = { hint: "local" } as const;
13
+ readonly icon = "lucide:flask-conical" as const;
14
+ readonly declaredOutputPorts = ["true", "false"] as const;
15
+ readonly name: string;
16
+ readonly id?: string;
17
+
18
+ constructor(name: string = "Is test run?", id?: string) {
19
+ this.name = name;
20
+ this.id = id;
21
+ }
22
+ }
23
+
24
+ export { IsTestRunNode } from "./IsTestRunNode";
@@ -16,6 +16,7 @@ export class MapData<TInputJson = unknown, TOutputJson = unknown> implements Run
16
16
  readonly execution = { hint: "local" } as const;
17
17
  /** Zero mapped items should still allow downstream nodes to run. */
18
18
  readonly continueWhenEmptyOutput = true as const;
19
+ readonly icon = "lucide:square-pen" as const;
19
20
  readonly keepBinaries: boolean;
20
21
 
21
22
  constructor(
@@ -10,7 +10,7 @@ export class Merge<TInputJson = unknown, TOutputJson = TInputJson> implements Ru
10
10
  > {
11
11
  readonly kind = "node" as const;
12
12
  readonly type: TypeToken<unknown> = MergeNode;
13
- readonly icon = "lucide:git-merge" as const;
13
+ readonly icon = "lucide:merge@rot=90" as const;
14
14
 
15
15
  constructor(
16
16
  public readonly name: string,
package/src/nodes/noOp.ts CHANGED
@@ -6,6 +6,7 @@ export class NoOp<TItemJson = unknown> implements RunnableNodeConfig<TItemJson,
6
6
  readonly kind = "node" as const;
7
7
  readonly type: TypeToken<unknown> = NoOpNode;
8
8
  readonly execution = { hint: "local" } as const;
9
+ readonly icon = "lucide:circle-dashed" as const;
9
10
 
10
11
  constructor(
11
12
  public readonly name: string = "NoOp",
@@ -12,7 +12,7 @@ export class Split<TIn = unknown, TElem = unknown> implements RunnableNodeConfig
12
12
  * Mirrors {@link MapData}'s empty-output behavior.
13
13
  */
14
14
  readonly continueWhenEmptyOutput = true as const;
15
- readonly icon = "lucide:ungroup" as const;
15
+ readonly icon = "builtin:split-rows" as const;
16
16
 
17
17
  constructor(
18
18
  public readonly name: string,
@@ -0,0 +1,72 @@
1
+ import type {
2
+ CredentialRequirement,
3
+ Item,
4
+ TestTriggerNodeConfig,
5
+ TestTriggerSetupContext,
6
+ TypeToken,
7
+ } from "@codemation/core";
8
+
9
+ import { TestTriggerNode } from "./TestTriggerNode";
10
+
11
+ export interface TestTriggerOptions<TOutputJson> {
12
+ readonly name?: string;
13
+ readonly id?: string;
14
+ readonly icon?: string;
15
+ /** Cap on simultaneous in-flight test cases for one suite run. Default: 4 (orchestrator). */
16
+ readonly concurrency?: number;
17
+ readonly credentialRequirements?: ReadonlyArray<CredentialRequirement>;
18
+ /**
19
+ * Free-form description of where the test cases come from. Shown in the node properties
20
+ * panel and the Tests-tab suite-detail header so authors revisiting the workflow six months
21
+ * later remember which mailbox / folder / fixture file the cases originate from.
22
+ */
23
+ readonly description?: string;
24
+ /**
25
+ * Author callback that yields one item per test case. Items are dispatched as separate
26
+ * workflow runs by the TestSuiteOrchestrator, with `executionOptions.testContext` set.
27
+ * The provided context exposes credential resolution and an AbortSignal for cancellation.
28
+ */
29
+ generateItems(ctx: TestTriggerSetupContext<TestTrigger<TOutputJson>>): AsyncIterable<Item<TOutputJson>>;
30
+ /**
31
+ * Optional resolver: extract a human-readable label from a yielded item. The orchestrator
32
+ * persists this on the run, so the Tests-tab tree-table shows e.g. "RFQ for batch 14"
33
+ * instead of an opaque runId. Typical use: `(item) => item.json.subject` for mailbox tests.
34
+ */
35
+ caseLabel?(item: Item<TOutputJson>): string | undefined;
36
+ }
37
+
38
+ /**
39
+ * Trigger config for a test fixture source. Drop one (or more) of these on the canvas alongside
40
+ * a workflow's live triggers; clicking "Run tests" on the Tests tab invokes
41
+ * {@link TestTriggerOptions.generateItems} via the TestSuiteOrchestrator.
42
+ */
43
+ export class TestTrigger<TOutputJson = unknown> implements TestTriggerNodeConfig<TOutputJson> {
44
+ readonly kind = "trigger" as const;
45
+ readonly triggerKind = "test" as const;
46
+ readonly type: TypeToken<unknown> = TestTriggerNode;
47
+ readonly icon: string;
48
+ readonly name: string;
49
+ readonly id?: string;
50
+ readonly concurrency?: number;
51
+ readonly description?: string;
52
+ readonly generateItems: TestTriggerOptions<TOutputJson>["generateItems"];
53
+ readonly caseLabel?: TestTriggerOptions<TOutputJson>["caseLabel"];
54
+ private readonly credentialRequirements: ReadonlyArray<CredentialRequirement>;
55
+
56
+ constructor(options: TestTriggerOptions<TOutputJson>) {
57
+ this.name = options.name ?? "Test trigger";
58
+ this.id = options.id;
59
+ this.icon = options.icon ?? "lucide:flask-conical";
60
+ this.concurrency = options.concurrency;
61
+ this.description = options.description;
62
+ this.credentialRequirements = options.credentialRequirements ?? [];
63
+ this.generateItems = options.generateItems;
64
+ this.caseLabel = options.caseLabel;
65
+ }
66
+
67
+ getCredentialRequirements(): ReadonlyArray<CredentialRequirement> {
68
+ return this.credentialRequirements;
69
+ }
70
+ }
71
+
72
+ export { TestTriggerNode } from "./TestTriggerNode";
package/src/nodes/wait.ts CHANGED
@@ -8,6 +8,7 @@ export class Wait<TItemJson = unknown> implements RunnableNodeConfig<TItemJson,
8
8
  readonly execution = { hint: "local" } as const;
9
9
  /** Pass-through empty batches should still advance to downstream nodes. */
10
10
  readonly continueWhenEmptyOutput = true as const;
11
+ readonly icon = "lucide:hourglass" as const;
11
12
 
12
13
  constructor(
13
14
  public readonly name: string,
@@ -1,46 +0,0 @@
1
- import type { ChatModelConfig, ChatModelStructuredOutputOptions } from "@codemation/core";
2
- import { injectable } from "@codemation/core";
3
-
4
- import { OpenAIChatModelFactory } from "./OpenAIChatModelFactory";
5
-
6
- @injectable()
7
- export class OpenAIStructuredOutputMethodFactory {
8
- private static readonly isoDatePattern = /^\d{4}-\d{2}-\d{2}$/;
9
-
10
- create(chatModelConfig: ChatModelConfig): ChatModelStructuredOutputOptions | undefined {
11
- if (chatModelConfig.type !== OpenAIChatModelFactory) {
12
- return undefined;
13
- }
14
- const model = this.readModelName(chatModelConfig);
15
- if (!model) {
16
- return { method: "functionCalling", strict: true };
17
- }
18
- return {
19
- method: this.supportsJsonSchema(model) ? "jsonSchema" : "functionCalling",
20
- strict: true,
21
- };
22
- }
23
-
24
- private readModelName(chatModelConfig: ChatModelConfig): string | undefined {
25
- const candidate = chatModelConfig as Readonly<{ model?: unknown }>;
26
- return typeof candidate.model === "string" ? candidate.model : undefined;
27
- }
28
-
29
- private supportsJsonSchema(model: string): boolean {
30
- if (model === "gpt-4o" || model === "gpt-4o-mini") {
31
- return true;
32
- }
33
- return (
34
- this.supportsSnapshotAtOrAfter(model, "gpt-4o-", "2024-08-06") ||
35
- this.supportsSnapshotAtOrAfter(model, "gpt-4o-mini-", "2024-07-18")
36
- );
37
- }
38
-
39
- private supportsSnapshotAtOrAfter(model: string, prefix: string, minimumSnapshotDate: string): boolean {
40
- if (!model.startsWith(prefix)) {
41
- return false;
42
- }
43
- const snapshotDate = model.slice(prefix.length);
44
- return OpenAIStructuredOutputMethodFactory.isoDatePattern.test(snapshotDate) && snapshotDate >= minimumSnapshotDate;
45
- }
46
- }