@codemation/core-nodes 1.0.1 → 1.1.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 +130 -0
- package/dist/index.cjs +3002 -65
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1521 -551
- package/dist/index.d.ts +1521 -551
- package/dist/index.js +2969 -73
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
- package/src/authoring/defineRestNode.types.ts +204 -0
- package/src/credentials/ApiKeyCredentialType.ts +60 -0
- package/src/credentials/BasicAuthCredentialType.ts +51 -0
- package/src/credentials/BearerTokenCredentialType.ts +40 -0
- package/src/credentials/OAuth2ClientCredentialsTypeFactory.ts +117 -0
- package/src/credentials/OAuth2TokenExchangeFactory.ts +52 -0
- package/src/credentials/index.ts +4 -0
- package/src/http/HttpBodyBuilder.ts +90 -0
- package/src/http/HttpRequestExecutor.ts +150 -0
- package/src/http/HttpUrlBuilder.ts +22 -0
- package/src/http/httpRequest.types.ts +69 -0
- package/src/index.ts +9 -0
- package/src/nodes/AIAgentNode.ts +101 -3
- package/src/nodes/AgentToolExecutionCoordinator.ts +29 -3
- package/src/nodes/AssertionNode.ts +42 -0
- package/src/nodes/CronTriggerFactory.ts +45 -0
- package/src/nodes/CronTriggerNode.ts +40 -0
- package/src/nodes/HttpRequestNodeFactory.ts +99 -23
- package/src/nodes/IsTestRunNode.ts +25 -0
- package/src/nodes/NodeBackedToolRuntime.ts +40 -4
- package/src/nodes/TestTriggerNode.ts +33 -0
- package/src/nodes/aiAgentSupport.types.ts +18 -3
- package/src/nodes/assertion.ts +42 -0
- package/src/nodes/collections/collectionDeleteNode.types.ts +23 -0
- package/src/nodes/collections/collectionFindOneNode.types.ts +26 -0
- package/src/nodes/collections/collectionGetNode.types.ts +26 -0
- package/src/nodes/collections/collectionInsertNode.types.ts +22 -0
- package/src/nodes/collections/collectionListNode.types.ts +30 -0
- package/src/nodes/collections/collectionUpdateNode.types.ts +23 -0
- package/src/nodes/collections/index.ts +6 -0
- package/src/nodes/httpRequest.ts +61 -1
- package/src/nodes/isTestRun.ts +24 -0
- package/src/nodes/testTrigger.ts +72 -0
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { Item, NodeExecutionContext, RunnableNode, RunnableNodeExecuteArgs } from "@codemation/core";
|
|
2
2
|
|
|
3
3
|
import { node } from "@codemation/core";
|
|
4
|
-
|
|
4
|
+
import type { CredentialSession, HttpRequestSpec } from "../http/httpRequest.types";
|
|
5
|
+
import { HttpRequestExecutor } from "../http/HttpRequestExecutor";
|
|
6
|
+
import { HttpBodyBuilder } from "../http/HttpBodyBuilder";
|
|
7
|
+
import { HttpUrlBuilder } from "../http/HttpUrlBuilder";
|
|
5
8
|
import type { HttpRequestDownloadMode } from "./httpRequest";
|
|
6
9
|
import { HttpRequest } from "./httpRequest";
|
|
7
10
|
|
|
@@ -16,44 +19,113 @@ export class HttpRequestNode implements RunnableNode<HttpRequest<any, any>> {
|
|
|
16
19
|
|
|
17
20
|
private async executeItem(item: Item, ctx: NodeExecutionContext<HttpRequest<any, any>>): Promise<Item> {
|
|
18
21
|
const url = this.resolveUrl(item, ctx);
|
|
19
|
-
const
|
|
22
|
+
const credential = await this.resolveCredential(ctx);
|
|
23
|
+
|
|
24
|
+
const spec: HttpRequestSpec = {
|
|
25
|
+
url,
|
|
20
26
|
method: ctx.config.method,
|
|
21
|
-
|
|
27
|
+
headers: ctx.config.args.headers,
|
|
28
|
+
query: ctx.config.args.query,
|
|
29
|
+
body: ctx.config.args.body,
|
|
30
|
+
credential,
|
|
31
|
+
download: {
|
|
32
|
+
mode: ctx.config.downloadMode,
|
|
33
|
+
binaryName: ctx.config.binaryName,
|
|
34
|
+
},
|
|
35
|
+
ctx: ctx as unknown as HttpRequestSpec["ctx"],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Build the request (headers, body encoding, URL query merge) once,
|
|
39
|
+
// then make a SINGLE fetch call and decide what to do with the response.
|
|
40
|
+
// This avoids a double-fetch regression for auto-mode binary responses.
|
|
41
|
+
const executor = new HttpRequestExecutor(globalThis.fetch, new HttpBodyBuilder(), new HttpUrlBuilder());
|
|
42
|
+
const { url: resolvedUrl, init } = await executor.buildRequest(spec, item);
|
|
43
|
+
|
|
44
|
+
const response = await globalThis.fetch(resolvedUrl, init);
|
|
45
|
+
|
|
22
46
|
const headers = this.readHeaders(response.headers);
|
|
23
47
|
const mimeType = this.resolveMimeType(headers);
|
|
24
|
-
const
|
|
25
|
-
const
|
|
48
|
+
const binaryName = ctx.config.binaryName;
|
|
49
|
+
const shouldAttach = this.shouldAttachBody(ctx.config.downloadMode, mimeType);
|
|
50
|
+
|
|
51
|
+
if (shouldAttach) {
|
|
52
|
+
const outputJson: Readonly<Record<string, unknown>> = {
|
|
53
|
+
url: resolvedUrl,
|
|
54
|
+
method: ctx.config.method,
|
|
55
|
+
ok: response.ok,
|
|
56
|
+
status: response.status,
|
|
57
|
+
statusText: response.statusText,
|
|
58
|
+
mimeType,
|
|
59
|
+
headers,
|
|
60
|
+
bodyBinaryName: binaryName,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const attachment = await ctx.binary.attach({
|
|
64
|
+
name: binaryName,
|
|
65
|
+
body: response.body
|
|
66
|
+
? (response.body as unknown as Parameters<typeof ctx.binary.attach>[0]["body"])
|
|
67
|
+
: new Uint8Array(await response.arrayBuffer()),
|
|
68
|
+
mimeType,
|
|
69
|
+
filename: this.resolveFilename(resolvedUrl, headers),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
let outputItem: Item = { json: outputJson };
|
|
73
|
+
outputItem = ctx.binary.withAttachment(outputItem, binaryName, attachment);
|
|
74
|
+
return outputItem;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Non-binary path: parse JSON or read text.
|
|
78
|
+
const isJson = this.isJsonMimeType(mimeType);
|
|
79
|
+
let json: unknown | undefined;
|
|
80
|
+
let text: string | undefined;
|
|
81
|
+
|
|
82
|
+
if (isJson) {
|
|
83
|
+
try {
|
|
84
|
+
json = await response.json();
|
|
85
|
+
} catch {
|
|
86
|
+
text = await response.text();
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
text = await response.text();
|
|
90
|
+
}
|
|
91
|
+
|
|
26
92
|
const outputJson: Readonly<Record<string, unknown>> = {
|
|
27
|
-
url,
|
|
93
|
+
url: resolvedUrl,
|
|
28
94
|
method: ctx.config.method,
|
|
29
95
|
ok: response.ok,
|
|
30
96
|
status: response.status,
|
|
31
97
|
statusText: response.statusText,
|
|
32
98
|
mimeType,
|
|
33
99
|
headers,
|
|
34
|
-
...(
|
|
100
|
+
...(json !== undefined ? { json } : {}),
|
|
101
|
+
...(text !== undefined ? { text } : {}),
|
|
35
102
|
};
|
|
36
103
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
};
|
|
40
|
-
if (!shouldAttachBody) {
|
|
41
|
-
return outputItem;
|
|
42
|
-
}
|
|
104
|
+
return { json: outputJson };
|
|
105
|
+
}
|
|
43
106
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
107
|
+
private async resolveCredential(
|
|
108
|
+
ctx: NodeExecutionContext<HttpRequest<any, any>>,
|
|
109
|
+
): Promise<CredentialSession | undefined> {
|
|
110
|
+
const slotKey = ctx.config.args.credentialSlot;
|
|
111
|
+
if (!slotKey) {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
return await ctx.getCredential<CredentialSession>(slotKey);
|
|
116
|
+
} catch {
|
|
117
|
+
// Credential slot configured but not bound — treat as no credential.
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
54
120
|
}
|
|
55
121
|
|
|
56
122
|
private resolveUrl(item: Item, ctx: NodeExecutionContext<HttpRequest<any, any>>): string {
|
|
123
|
+
// Literal URL in args takes precedence over the legacy urlField approach.
|
|
124
|
+
const literalUrl = ctx.config.args.url;
|
|
125
|
+
if (literalUrl && literalUrl.trim().length > 0) {
|
|
126
|
+
return literalUrl.trim();
|
|
127
|
+
}
|
|
128
|
+
|
|
57
129
|
const json = this.asRecord(item.json);
|
|
58
130
|
const candidate = json[ctx.config.urlField];
|
|
59
131
|
if (typeof candidate !== "string" || candidate.trim() === "") {
|
|
@@ -85,6 +157,10 @@ export class HttpRequestNode implements RunnableNode<HttpRequest<any, any>> {
|
|
|
85
157
|
return contentType.split(";")[0]?.trim() || "application/octet-stream";
|
|
86
158
|
}
|
|
87
159
|
|
|
160
|
+
private isJsonMimeType(mimeType: string): boolean {
|
|
161
|
+
return mimeType === "application/json" || mimeType.endsWith("+json");
|
|
162
|
+
}
|
|
163
|
+
|
|
88
164
|
private shouldAttachBody(mode: HttpRequestDownloadMode, mimeType: string): boolean {
|
|
89
165
|
if (mode === "always") {
|
|
90
166
|
return true;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { RunnableNode, RunnableNodeExecuteArgs } from "@codemation/core";
|
|
2
|
+
import { emitPorts, node } from "@codemation/core";
|
|
3
|
+
|
|
4
|
+
import { IsTestRun } from "./isTestRun";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Routes each item to the `true` port if `ctx.testContext` is set (the run was started by the
|
|
8
|
+
* TestSuiteOrchestrator), else to `false`. Lets workflow authors guard real side-effects:
|
|
9
|
+
*
|
|
10
|
+
* GmailTrigger / TestTrigger → ClassifyAgent → IsTestRun
|
|
11
|
+
* ├── true → AssertionNode
|
|
12
|
+
* └── false → SendReply
|
|
13
|
+
*/
|
|
14
|
+
@node({ packageName: "@codemation/core-nodes" })
|
|
15
|
+
export class IsTestRunNode implements RunnableNode<IsTestRun<unknown>> {
|
|
16
|
+
kind = "node" as const;
|
|
17
|
+
|
|
18
|
+
execute(args: RunnableNodeExecuteArgs<IsTestRun<unknown>>): unknown {
|
|
19
|
+
const isTest = args.ctx.testContext !== undefined;
|
|
20
|
+
return emitPorts({
|
|
21
|
+
true: isTest ? [args.item] : [],
|
|
22
|
+
false: isTest ? [] : [args.item],
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -11,6 +11,8 @@ import type {
|
|
|
11
11
|
ZodSchemaAny,
|
|
12
12
|
} from "@codemation/core";
|
|
13
13
|
import {
|
|
14
|
+
AgentConfigInspector,
|
|
15
|
+
ChildExecutionScopeFactory,
|
|
14
16
|
CoreTokens,
|
|
15
17
|
inject,
|
|
16
18
|
injectable,
|
|
@@ -31,6 +33,8 @@ export class NodeBackedToolRuntime {
|
|
|
31
33
|
private readonly outputNormalizer: NodeOutputNormalizer,
|
|
32
34
|
@inject(RunnableOutputBehaviorResolver)
|
|
33
35
|
private readonly outputBehaviorResolver: RunnableOutputBehaviorResolver,
|
|
36
|
+
@inject(ChildExecutionScopeFactory)
|
|
37
|
+
private readonly childExecutionScopeFactory: ChildExecutionScopeFactory,
|
|
34
38
|
) {}
|
|
35
39
|
|
|
36
40
|
async execute(
|
|
@@ -45,10 +49,7 @@ export class NodeBackedToolRuntime {
|
|
|
45
49
|
ctx: args.ctx,
|
|
46
50
|
node: config.node,
|
|
47
51
|
});
|
|
48
|
-
const nodeCtx =
|
|
49
|
-
...args.ctx,
|
|
50
|
-
config: config.node,
|
|
51
|
-
} as NodeExecutionContext<RunnableNodeConfig>;
|
|
52
|
+
const nodeCtx = this.resolveNodeCtx(config, args);
|
|
52
53
|
const resolvedNode = this.nodeResolver.resolve(config.node.type);
|
|
53
54
|
const outputs = await this.executeResolvedNode(resolvedNode, nodeInput, nodeCtx);
|
|
54
55
|
return config.toToolOutput({
|
|
@@ -62,6 +63,41 @@ export class NodeBackedToolRuntime {
|
|
|
62
63
|
});
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Returns a re-rooted child ctx for nested-agent tools (so their LLM/tool connection ids derive
|
|
68
|
+
* from the tool connection node, telemetry parents under the tool-call span, and connection
|
|
69
|
+
* invocations carry `parentInvocationId`). Plain runnable tools (non-agent) keep the orchestrator
|
|
70
|
+
* ctx with only `config` swapped — no nesting concern.
|
|
71
|
+
*
|
|
72
|
+
* The caller (`AIAgentNode.createItemScopedTools`) already wraps the orchestrator ctx via
|
|
73
|
+
* `ConnectionCredentialExecutionContextFactory.forConnectionNode`, so `args.ctx.nodeId` is the
|
|
74
|
+
* tool's own connection node id (e.g. `AIAgentNode:2__conn__tool__searchInMail`). We pass that
|
|
75
|
+
* through as the sub-agent's `nodeId`; deriving another `toolConnectionNodeId(args.ctx.nodeId,
|
|
76
|
+
* config.name)` here would prepend a duplicate `__conn__tool__<name>` segment and exponentially
|
|
77
|
+
* deepen ids on each invocation, which also breaks credential resolution because user-provided
|
|
78
|
+
* bindings sit on the single-level connection node id.
|
|
79
|
+
*/
|
|
80
|
+
private resolveNodeCtx(
|
|
81
|
+
config: NodeBackedToolConfig<any, ZodSchemaAny, ZodSchemaAny>,
|
|
82
|
+
args: ToolExecuteArgs,
|
|
83
|
+
): NodeExecutionContext<RunnableNodeConfig> {
|
|
84
|
+
const isNestedAgent = AgentConfigInspector.isAgentNodeConfig(config.node);
|
|
85
|
+
const hooks = args.hooks;
|
|
86
|
+
if (!isNestedAgent || !hooks?.parentSpan || !hooks.parentInvocationId) {
|
|
87
|
+
return {
|
|
88
|
+
...args.ctx,
|
|
89
|
+
config: config.node,
|
|
90
|
+
} as NodeExecutionContext<RunnableNodeConfig>;
|
|
91
|
+
}
|
|
92
|
+
return this.childExecutionScopeFactory.forSubAgent({
|
|
93
|
+
parentCtx: args.ctx as NodeExecutionContext<RunnableNodeConfig>,
|
|
94
|
+
childNodeId: args.ctx.nodeId,
|
|
95
|
+
childConfig: config.node as unknown as RunnableNodeConfig,
|
|
96
|
+
parentInvocationId: hooks.parentInvocationId,
|
|
97
|
+
parentSpan: hooks.parentSpan,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
65
101
|
private async executeResolvedNode(
|
|
66
102
|
resolvedNode: unknown,
|
|
67
103
|
nodeInput: ToolExecuteArgs["item"],
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Items,
|
|
3
|
+
NodeExecutionContext,
|
|
4
|
+
NodeOutputs,
|
|
5
|
+
TestTriggerNodeConfig,
|
|
6
|
+
TriggerNode,
|
|
7
|
+
TriggerSetupContext,
|
|
8
|
+
} from "@codemation/core";
|
|
9
|
+
|
|
10
|
+
import { node } from "@codemation/core";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Author-defined test-fixture trigger. Live activation skips this trigger (filtered by
|
|
14
|
+
* `triggerKind === "test"` in `TriggerRuntimeService`); the `TestSuiteOrchestrator` drives its
|
|
15
|
+
* `generateItems` callback during a TestSuiteRun and dispatches one workflow run per yielded item.
|
|
16
|
+
*
|
|
17
|
+
* `setup` is intentionally a no-op for symmetry with other trigger nodes — the real work happens
|
|
18
|
+
* in the orchestrator. `execute` is a passthrough so items provided to `engine.runWorkflow(...)`
|
|
19
|
+
* (one per case) flow downstream unchanged on `main`.
|
|
20
|
+
*/
|
|
21
|
+
@node({ packageName: "@codemation/core-nodes" })
|
|
22
|
+
export class TestTriggerNode implements TriggerNode<TestTriggerNodeConfig<any>> {
|
|
23
|
+
kind = "trigger" as const;
|
|
24
|
+
outputPorts = ["main"] as const;
|
|
25
|
+
|
|
26
|
+
async setup(_ctx: TriggerSetupContext<TestTriggerNodeConfig<any>>): Promise<undefined> {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async execute(items: Items, _ctx: NodeExecutionContext<TestTriggerNodeConfig<any>>): Promise<NodeOutputs> {
|
|
31
|
+
return { main: items };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AgentToolCall,
|
|
3
|
+
ConnectionInvocationId,
|
|
3
4
|
Item,
|
|
4
5
|
NodeInputsByPort,
|
|
6
|
+
TelemetrySpanScope,
|
|
5
7
|
ToolConfig,
|
|
6
8
|
ToolExecuteArgs,
|
|
7
9
|
ZodSchemaAny,
|
|
@@ -24,13 +26,24 @@ export type ResolvedTool = Readonly<{
|
|
|
24
26
|
|
|
25
27
|
/**
|
|
26
28
|
* Per-item binding of a tool: the user config plus the resolved runtime and a snapshot of the
|
|
27
|
-
* original Zod `inputSchema
|
|
28
|
-
*
|
|
29
|
+
* original Zod `inputSchema`.
|
|
30
|
+
*
|
|
31
|
+
* `execute` accepts optional `hooks` so the agent coordinator can pass the live `agent.tool.call`
|
|
32
|
+
* span and the planned tool-call's `invocationId`. Node-backed sub-agent tools use these hooks
|
|
33
|
+
* via {@link ChildExecutionScopeFactory} to re-root their runtime ctx under the tool-call boundary
|
|
34
|
+
* (fresh activationId, telemetry parented at the tool-call span, `parentInvocationId` set).
|
|
29
35
|
*/
|
|
30
36
|
export type ItemScopedToolBinding = Readonly<{
|
|
31
37
|
config: ToolConfig;
|
|
32
38
|
inputSchema: ZodSchemaAny;
|
|
33
|
-
execute(input: unknown): Promise<unknown>;
|
|
39
|
+
execute(input: unknown, hooks?: ItemScopedToolCallHooks): Promise<unknown>;
|
|
40
|
+
}>;
|
|
41
|
+
|
|
42
|
+
export type ItemScopedToolCallHooks = Readonly<{
|
|
43
|
+
/** Live agent.tool.call span (used to parent sub-agent telemetry). */
|
|
44
|
+
parentSpan?: TelemetrySpanScope;
|
|
45
|
+
/** invocationId of the parent tool call (used to thread `parentInvocationId` through ctx). */
|
|
46
|
+
parentInvocationId?: ConnectionInvocationId;
|
|
34
47
|
}>;
|
|
35
48
|
|
|
36
49
|
export type PlannedToolCall = Readonly<{
|
|
@@ -38,6 +51,8 @@ export type PlannedToolCall = Readonly<{
|
|
|
38
51
|
toolCall: AgentToolCall;
|
|
39
52
|
invocationIndex: number;
|
|
40
53
|
nodeId: string;
|
|
54
|
+
/** Stable id reused across queued / running / completed connection invocation rows for this tool call. */
|
|
55
|
+
invocationId: string;
|
|
41
56
|
}>;
|
|
42
57
|
|
|
43
58
|
export type ExecutedToolCall = Readonly<{
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { AssertionResult, Item, NodeExecutionContext, RunnableNodeConfig, TypeToken } from "@codemation/core";
|
|
2
|
+
|
|
3
|
+
import { AssertionNode } from "./AssertionNode";
|
|
4
|
+
|
|
5
|
+
export interface AssertionOptions<TInputJson> {
|
|
6
|
+
readonly name?: string;
|
|
7
|
+
readonly id?: string;
|
|
8
|
+
readonly icon?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Author callback. Returns one or more {@link AssertionResult}s per input item. Each becomes
|
|
11
|
+
* one emitted output item — useful for per-row reporting in the Tests tab. Return `[]` to
|
|
12
|
+
* emit nothing for this case (rare; usually you want at least a "no-op" pass).
|
|
13
|
+
*/
|
|
14
|
+
assertions(
|
|
15
|
+
item: Item<TInputJson>,
|
|
16
|
+
ctx: NodeExecutionContext<Assertion<TInputJson>>,
|
|
17
|
+
): Promise<ReadonlyArray<AssertionResult>> | ReadonlyArray<AssertionResult>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generic assertion node — the "callback" form. For declarative shorthands (StringEquals,
|
|
22
|
+
* JudgeByAgent) compose this with helpers added in later phases. Sets `emitsAssertions: true`
|
|
23
|
+
* so host-side persisters know to record its outputs as `TestAssertion` rows.
|
|
24
|
+
*/
|
|
25
|
+
export class Assertion<TInputJson = unknown> implements RunnableNodeConfig<TInputJson, AssertionResult> {
|
|
26
|
+
readonly kind = "node" as const;
|
|
27
|
+
readonly type: TypeToken<unknown> = AssertionNode;
|
|
28
|
+
readonly icon: string;
|
|
29
|
+
readonly name: string;
|
|
30
|
+
readonly id?: string;
|
|
31
|
+
readonly emitsAssertions = true as const;
|
|
32
|
+
readonly assertions: AssertionOptions<TInputJson>["assertions"];
|
|
33
|
+
|
|
34
|
+
constructor(options: AssertionOptions<TInputJson>) {
|
|
35
|
+
this.name = options.name ?? "Assertion";
|
|
36
|
+
this.id = options.id;
|
|
37
|
+
this.icon = options.icon ?? "lucide:check-circle";
|
|
38
|
+
this.assertions = options.assertions;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { AssertionNode } from "./AssertionNode";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { defineNode } from "@codemation/core";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const collectionDeleteNode = defineNode({
|
|
5
|
+
key: "collection-delete",
|
|
6
|
+
title: "Collection: Delete",
|
|
7
|
+
description: "Delete a row by id from a collection.",
|
|
8
|
+
icon: "lucide:braces",
|
|
9
|
+
configSchema: z.object({
|
|
10
|
+
collectionName: z.string(),
|
|
11
|
+
id: z.string(),
|
|
12
|
+
}),
|
|
13
|
+
async execute(_args, { config, execution }) {
|
|
14
|
+
const store = execution.collections?.[config.collectionName];
|
|
15
|
+
if (!store) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`Collection "${config.collectionName}" is not registered. Add defineCollection to your codemation config.`,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
const result = await store.delete(config.id);
|
|
21
|
+
return { deleted: result.deleted, id: config.id };
|
|
22
|
+
},
|
|
23
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineNode } from "@codemation/core";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const collectionFindOneNode = defineNode({
|
|
5
|
+
key: "collection-find-one",
|
|
6
|
+
title: "Collection: Find One",
|
|
7
|
+
description: "Find a single row matching a filter in a collection.",
|
|
8
|
+
icon: "lucide:filter",
|
|
9
|
+
configSchema: z.object({
|
|
10
|
+
collectionName: z.string(),
|
|
11
|
+
where: z.record(z.string(), z.unknown()),
|
|
12
|
+
}),
|
|
13
|
+
async execute(_args, { config, execution }) {
|
|
14
|
+
const store = execution.collections?.[config.collectionName];
|
|
15
|
+
if (!store) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`Collection "${config.collectionName}" is not registered. Add defineCollection to your codemation config.`,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
const row = await store.findOne(config.where);
|
|
21
|
+
if (row === null) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
return row;
|
|
25
|
+
},
|
|
26
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineNode } from "@codemation/core";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const collectionGetNode = defineNode({
|
|
5
|
+
key: "collection-get",
|
|
6
|
+
title: "Collection: Get",
|
|
7
|
+
description: "Get a single row by id from a collection.",
|
|
8
|
+
icon: "lucide:layers",
|
|
9
|
+
configSchema: z.object({
|
|
10
|
+
collectionName: z.string(),
|
|
11
|
+
id: z.string(),
|
|
12
|
+
}),
|
|
13
|
+
async execute(_args, { config, execution }) {
|
|
14
|
+
const store = execution.collections?.[config.collectionName];
|
|
15
|
+
if (!store) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`Collection "${config.collectionName}" is not registered. Add defineCollection to your codemation config.`,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
const row = await store.get(config.id);
|
|
21
|
+
if (row === null) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
return row;
|
|
25
|
+
},
|
|
26
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineNode } from "@codemation/core";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const collectionInsertNode = defineNode({
|
|
5
|
+
key: "collection-insert",
|
|
6
|
+
title: "Collection: Insert",
|
|
7
|
+
description: "Insert a new row into a collection.",
|
|
8
|
+
icon: "lucide:boxes",
|
|
9
|
+
configSchema: z.object({
|
|
10
|
+
collectionName: z.string(),
|
|
11
|
+
data: z.record(z.string(), z.unknown()),
|
|
12
|
+
}),
|
|
13
|
+
async execute(_args, { config, execution }) {
|
|
14
|
+
const store = execution.collections?.[config.collectionName];
|
|
15
|
+
if (!store) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`Collection "${config.collectionName}" is not registered. Add defineCollection to your codemation config.`,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
return await store.insert(config.data);
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { defineNode } from "@codemation/core";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const collectionListNode = defineNode({
|
|
5
|
+
key: "collection-list",
|
|
6
|
+
title: "Collection: List",
|
|
7
|
+
description: "List rows from a collection with optional pagination and filtering.",
|
|
8
|
+
icon: "lucide:split",
|
|
9
|
+
configSchema: z.object({
|
|
10
|
+
collectionName: z.string(),
|
|
11
|
+
limit: z.number().int().positive().optional(),
|
|
12
|
+
offset: z.number().int().nonnegative().optional(),
|
|
13
|
+
where: z.record(z.string(), z.unknown()).optional(),
|
|
14
|
+
}),
|
|
15
|
+
async execute(_args, { config, execution }) {
|
|
16
|
+
const store = execution.collections?.[config.collectionName];
|
|
17
|
+
if (!store) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`Collection "${config.collectionName}" is not registered. Add defineCollection to your codemation config.`,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
const { rows } = await store.list({
|
|
23
|
+
limit: config.limit,
|
|
24
|
+
offset: config.offset,
|
|
25
|
+
where: config.where,
|
|
26
|
+
});
|
|
27
|
+
// Emit one item per row per AGENTS.md engine/node contract.
|
|
28
|
+
return [...rows];
|
|
29
|
+
},
|
|
30
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { defineNode } from "@codemation/core";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const collectionUpdateNode = defineNode({
|
|
5
|
+
key: "collection-update",
|
|
6
|
+
title: "Collection: Update",
|
|
7
|
+
description: "Update a row by id in a collection.",
|
|
8
|
+
icon: "lucide:square-pen",
|
|
9
|
+
configSchema: z.object({
|
|
10
|
+
collectionName: z.string(),
|
|
11
|
+
id: z.string(),
|
|
12
|
+
patch: z.record(z.string(), z.unknown()),
|
|
13
|
+
}),
|
|
14
|
+
async execute(_args, { config, execution }) {
|
|
15
|
+
const store = execution.collections?.[config.collectionName];
|
|
16
|
+
if (!store) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`Collection "${config.collectionName}" is not registered. Add defineCollection to your codemation config.`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
return await store.update(config.id, config.patch);
|
|
22
|
+
},
|
|
23
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { collectionInsertNode } from "./collectionInsertNode.types";
|
|
2
|
+
export { collectionGetNode } from "./collectionGetNode.types";
|
|
3
|
+
export { collectionFindOneNode } from "./collectionFindOneNode.types";
|
|
4
|
+
export { collectionListNode } from "./collectionListNode.types";
|
|
5
|
+
export { collectionUpdateNode } from "./collectionUpdateNode.types";
|
|
6
|
+
export { collectionDeleteNode } from "./collectionDeleteNode.types";
|
package/src/nodes/httpRequest.ts
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
|
-
import {
|
|
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,22 @@ 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;
|
|
17
32
|
}>;
|
|
18
33
|
|
|
34
|
+
/**
|
|
35
|
+
* The built-in HTTP request credential type IDs accepted by the `HttpRequest` node.
|
|
36
|
+
* These match the four generic credential types shipped with `@codemation/core-nodes`.
|
|
37
|
+
*/
|
|
38
|
+
export const HTTP_REQUEST_ACCEPTED_CREDENTIAL_TYPES: ReadonlyArray<string> = [
|
|
39
|
+
bearerTokenCredentialType.definition.typeId,
|
|
40
|
+
apiKeyCredentialType.definition.typeId,
|
|
41
|
+
basicAuthCredentialType.definition.typeId,
|
|
42
|
+
oauth2ClientCredentialsType.definition.typeId,
|
|
43
|
+
] as const;
|
|
44
|
+
|
|
19
45
|
export class HttpRequest<
|
|
20
46
|
TInputJson = Readonly<{ url?: string }>,
|
|
21
47
|
TOutputJson = HttpRequestOutputJson,
|
|
@@ -28,8 +54,27 @@ export class HttpRequest<
|
|
|
28
54
|
constructor(
|
|
29
55
|
public readonly name: string,
|
|
30
56
|
public readonly args: Readonly<{
|
|
57
|
+
/** HTTP method (default: GET). */
|
|
31
58
|
method?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Legacy: field name on item.json to read the URL from.
|
|
61
|
+
* Use `url` for a literal/templated URL instead.
|
|
62
|
+
*/
|
|
32
63
|
urlField?: string;
|
|
64
|
+
/** Literal or templated URL. When present, takes precedence over `urlField`. */
|
|
65
|
+
url?: string;
|
|
66
|
+
/** Extra headers to add to every request. */
|
|
67
|
+
headers?: Readonly<Record<string, string>>;
|
|
68
|
+
/** Query parameters to append to the URL. */
|
|
69
|
+
query?: Readonly<Record<string, string>>;
|
|
70
|
+
/** Request body specification. For canvas use, pass a JSON string in `body.data`. */
|
|
71
|
+
body?: HttpBodySpec;
|
|
72
|
+
/**
|
|
73
|
+
* Credential slot key. When set, the node resolves a credential via
|
|
74
|
+
* `ctx.getCredential(credentialSlot)` and applies it to the request.
|
|
75
|
+
* The slot must be declared in `getCredentialRequirements()`.
|
|
76
|
+
*/
|
|
77
|
+
credentialSlot?: string;
|
|
33
78
|
binaryName?: string;
|
|
34
79
|
downloadMode?: HttpRequestDownloadMode;
|
|
35
80
|
id?: string;
|
|
@@ -56,6 +101,21 @@ export class HttpRequest<
|
|
|
56
101
|
get downloadMode(): HttpRequestDownloadMode {
|
|
57
102
|
return this.args.downloadMode ?? "auto";
|
|
58
103
|
}
|
|
104
|
+
|
|
105
|
+
getCredentialRequirements(): ReadonlyArray<CredentialRequirement> {
|
|
106
|
+
if (!this.args.credentialSlot) {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
return [
|
|
110
|
+
{
|
|
111
|
+
slotKey: this.args.credentialSlot,
|
|
112
|
+
label: "Authentication",
|
|
113
|
+
acceptedTypes: HTTP_REQUEST_ACCEPTED_CREDENTIAL_TYPES,
|
|
114
|
+
optional: true,
|
|
115
|
+
helpText: "Optional credential for authenticating the HTTP request.",
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
}
|
|
59
119
|
}
|
|
60
120
|
|
|
61
121
|
export { HttpRequestNode } from "./HttpRequestNodeFactory";
|
|
@@ -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";
|