@n8n-as-code/transformer 0.2.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/README.md +215 -0
- package/dist/compiler/index.d.ts +6 -0
- package/dist/compiler/index.d.ts.map +1 -0
- package/dist/compiler/index.js +6 -0
- package/dist/compiler/index.js.map +1 -0
- package/dist/compiler/typescript-parser.d.ts +87 -0
- package/dist/compiler/typescript-parser.d.ts.map +1 -0
- package/dist/compiler/typescript-parser.js +379 -0
- package/dist/compiler/typescript-parser.js.map +1 -0
- package/dist/compiler/workflow-builder.d.ts +48 -0
- package/dist/compiler/workflow-builder.d.ts.map +1 -0
- package/dist/compiler/workflow-builder.js +231 -0
- package/dist/compiler/workflow-builder.js.map +1 -0
- package/dist/decorators/helpers.d.ts +27 -0
- package/dist/decorators/helpers.d.ts.map +1 -0
- package/dist/decorators/helpers.js +97 -0
- package/dist/decorators/helpers.js.map +1 -0
- package/dist/decorators/index.d.ts +11 -0
- package/dist/decorators/index.d.ts.map +1 -0
- package/dist/decorators/index.js +10 -0
- package/dist/decorators/index.js.map +1 -0
- package/dist/decorators/links.d.ts +70 -0
- package/dist/decorators/links.d.ts.map +1 -0
- package/dist/decorators/links.js +85 -0
- package/dist/decorators/links.js.map +1 -0
- package/dist/decorators/node.d.ts +32 -0
- package/dist/decorators/node.d.ts.map +1 -0
- package/dist/decorators/node.js +43 -0
- package/dist/decorators/node.js.map +1 -0
- package/dist/decorators/types.d.ts +122 -0
- package/dist/decorators/types.d.ts.map +1 -0
- package/dist/decorators/types.js +13 -0
- package/dist/decorators/types.js.map +1 -0
- package/dist/decorators/workflow.d.ts +32 -0
- package/dist/decorators/workflow.d.ts.map +1 -0
- package/dist/decorators/workflow.js +38 -0
- package/dist/decorators/workflow.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/ast-to-typescript.d.ts +55 -0
- package/dist/parser/ast-to-typescript.d.ts.map +1 -0
- package/dist/parser/ast-to-typescript.js +337 -0
- package/dist/parser/ast-to-typescript.js.map +1 -0
- package/dist/parser/index.d.ts +6 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +6 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/parser/json-to-ast.d.ts +61 -0
- package/dist/parser/json-to-ast.d.ts.map +1 -0
- package/dist/parser/json-to-ast.js +222 -0
- package/dist/parser/json-to-ast.js.map +1 -0
- package/dist/types.d.ts +227 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/formatting.d.ts +33 -0
- package/dist/utils/formatting.d.ts.map +1 -0
- package/dist/utils/formatting.js +83 -0
- package/dist/utils/formatting.js.map +1 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/naming.d.ts +39 -0
- package/dist/utils/naming.d.ts.map +1 -0
- package/dist/utils/naming.js +161 -0
- package/dist/utils/naming.js.map +1 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# @n8n-as-code/transformer
|
|
2
|
+
|
|
3
|
+
> **✨ NEW in v0.2.0**: This package enables the new **TypeScript workflow format** (`.workflow.ts`) that replaces JSON as the default storage format across n8n-as-code.
|
|
4
|
+
|
|
5
|
+
Bidirectional transformer for n8n workflows: JSON ↔ TypeScript
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
This package provides the core transformation engine that converts:
|
|
10
|
+
- **JSON → TypeScript**: n8n workflow JSON (from API) → TypeScript class with decorators
|
|
11
|
+
- **TypeScript → JSON**: TypeScript workflow class → n8n workflow JSON (for API)
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- ✅ **Bidirectional transformation** with roundtrip support
|
|
16
|
+
- ✅ **TypeScript decorators** for clean, readable workflow definitions
|
|
17
|
+
- ✅ **Auto-layout support** for AI-generated workflows (optional positions)
|
|
18
|
+
- ✅ **Name collision handling** (HttpRequest1, HttpRequest2, ...)
|
|
19
|
+
- ✅ **Prettier integration** for formatted output
|
|
20
|
+
- ✅ **AI dependency injection** syntax for LangChain nodes
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @n8n-as-code/transformer
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
### For Workflow Authors (TypeScript)
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { workflow, node, links } from '@n8n-as-code/transformer';
|
|
34
|
+
|
|
35
|
+
@workflow({
|
|
36
|
+
id: "unique-workflow-id",
|
|
37
|
+
name: "My Workflow",
|
|
38
|
+
active: true,
|
|
39
|
+
settings: { executionOrder: "v1" }
|
|
40
|
+
})
|
|
41
|
+
export class MyWorkflow {
|
|
42
|
+
|
|
43
|
+
@node({
|
|
44
|
+
name: "Schedule Trigger",
|
|
45
|
+
type: "n8n-nodes-base.scheduleTrigger",
|
|
46
|
+
version: 1.2,
|
|
47
|
+
position: [0, 0]
|
|
48
|
+
})
|
|
49
|
+
ScheduleTrigger = {
|
|
50
|
+
rule: {
|
|
51
|
+
interval: [{
|
|
52
|
+
field: "cronExpression",
|
|
53
|
+
expression: "0 9 * * 1-5"
|
|
54
|
+
}]
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
@node({
|
|
59
|
+
name: "HTTP Request",
|
|
60
|
+
type: "n8n-nodes-base.httpRequest",
|
|
61
|
+
version: 4,
|
|
62
|
+
position: [200, 0]
|
|
63
|
+
})
|
|
64
|
+
HttpRequest = {
|
|
65
|
+
url: "https://api.example.com/data",
|
|
66
|
+
method: "GET"
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
@links()
|
|
70
|
+
defineRouting() {
|
|
71
|
+
this.ScheduleTrigger.out(0).to(this.HttpRequest.in(0));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### For Package Developers (Transformation)
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import { JsonToAstParser, AstToTypeScriptGenerator } from '@n8n-as-code/transformer';
|
|
80
|
+
|
|
81
|
+
// JSON → TypeScript
|
|
82
|
+
const parser = new JsonToAstParser();
|
|
83
|
+
const ast = parser.parse(workflowJson);
|
|
84
|
+
|
|
85
|
+
const generator = new AstToTypeScriptGenerator();
|
|
86
|
+
const tsCode = await generator.generate(ast, {
|
|
87
|
+
format: true,
|
|
88
|
+
commentStyle: 'verbose'
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
console.log(tsCode); // Ready to write to .workflow.ts file
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import { TypeScriptParser, WorkflowBuilder } from '@n8n-as-code/transformer';
|
|
96
|
+
|
|
97
|
+
// TypeScript → JSON (Phase 2 - coming soon)
|
|
98
|
+
const parser = new TypeScriptParser();
|
|
99
|
+
const ast = await parser.parseFile('my-workflow.ts');
|
|
100
|
+
|
|
101
|
+
const builder = new WorkflowBuilder();
|
|
102
|
+
const workflowJson = builder.build(ast);
|
|
103
|
+
|
|
104
|
+
console.log(workflowJson); // Ready to push to n8n API
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Architecture
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
┌─────────────────────────────────────────────┐
|
|
111
|
+
│ │
|
|
112
|
+
│ n8n JSON Workflow (API) │
|
|
113
|
+
│ │
|
|
114
|
+
└──────────────┬──────────────────────────────┘
|
|
115
|
+
│
|
|
116
|
+
│ JsonToAstParser
|
|
117
|
+
▼
|
|
118
|
+
┌─────────────────────────────────────────────┐
|
|
119
|
+
│ │
|
|
120
|
+
│ WorkflowAST (Intermediate) │
|
|
121
|
+
│ - Normalized structure │
|
|
122
|
+
│ - Property names instead of UUIDs │
|
|
123
|
+
│ │
|
|
124
|
+
└──────────────┬──────────────────────────────┘
|
|
125
|
+
│
|
|
126
|
+
│ AstToTypeScriptGenerator
|
|
127
|
+
▼
|
|
128
|
+
┌─────────────────────────────────────────────┐
|
|
129
|
+
│ │
|
|
130
|
+
│ TypeScript Workflow (.workflow.ts) │
|
|
131
|
+
│ - Decorators (@workflow, @node, @links) │
|
|
132
|
+
│ - Human-readable property names │
|
|
133
|
+
│ - Formatted with Prettier │
|
|
134
|
+
│ │
|
|
135
|
+
└─────────────────────────────────────────────┘
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Status
|
|
139
|
+
|
|
140
|
+
- ✅ **Phase 1**: Architecture & decorators (COMPLETE)
|
|
141
|
+
- 🚧 **Phase 2**: Core transformation logic (IN PROGRESS)
|
|
142
|
+
- ⏳ **Phase 3**: Integration with @n8n-as-code/sync
|
|
143
|
+
- ⏳ **Phase 4**: Integration with @n8n-as-code/skills
|
|
144
|
+
|
|
145
|
+
## API Reference
|
|
146
|
+
|
|
147
|
+
### Decorators
|
|
148
|
+
|
|
149
|
+
#### `@workflow(metadata)`
|
|
150
|
+
|
|
151
|
+
Marks a class as an n8n workflow.
|
|
152
|
+
|
|
153
|
+
**Parameters:**
|
|
154
|
+
- `id`: Workflow ID (UUID)
|
|
155
|
+
- `name`: Workflow name
|
|
156
|
+
- `active`: Whether workflow is active
|
|
157
|
+
- `settings?`: Workflow settings (executionOrder, etc.)
|
|
158
|
+
|
|
159
|
+
#### `@node(metadata)`
|
|
160
|
+
|
|
161
|
+
Marks a property as an n8n node.
|
|
162
|
+
|
|
163
|
+
**Parameters:**
|
|
164
|
+
- `name`: Node display name
|
|
165
|
+
- `type`: Node type (e.g., "n8n-nodes-base.httpRequest")
|
|
166
|
+
- `version`: Node version
|
|
167
|
+
- `position?`: [x, y] coordinates (optional for auto-layout)
|
|
168
|
+
- `credentials?`: Node credentials
|
|
169
|
+
- `onError?`: Error handling behavior
|
|
170
|
+
|
|
171
|
+
#### `@links()`
|
|
172
|
+
|
|
173
|
+
Marks the method that defines workflow routing.
|
|
174
|
+
|
|
175
|
+
### Transformation Classes
|
|
176
|
+
|
|
177
|
+
#### `JsonToAstParser`
|
|
178
|
+
|
|
179
|
+
Parses n8n JSON to intermediate AST.
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
const parser = new JsonToAstParser();
|
|
183
|
+
const ast = parser.parse(workflowJson);
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
#### `AstToTypeScriptGenerator`
|
|
187
|
+
|
|
188
|
+
Generates TypeScript code from AST.
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
const generator = new AstToTypeScriptGenerator();
|
|
192
|
+
const code = await generator.generate(ast, options);
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Options:**
|
|
196
|
+
- `format?: boolean` - Apply Prettier formatting (default: true)
|
|
197
|
+
- `commentStyle?: 'minimal' | 'verbose'` - Comment style (default: 'verbose')
|
|
198
|
+
- `className?: string` - Custom class name
|
|
199
|
+
|
|
200
|
+
## Development
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
# Build
|
|
204
|
+
npm run build
|
|
205
|
+
|
|
206
|
+
# Tests
|
|
207
|
+
npm test
|
|
208
|
+
|
|
209
|
+
# Type check
|
|
210
|
+
npm run typecheck
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## License
|
|
214
|
+
|
|
215
|
+
See LICENSE in repository root.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/compiler/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/compiler/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript Parser
|
|
3
|
+
*
|
|
4
|
+
* Parses TypeScript workflow files using ts-morph
|
|
5
|
+
* Extracts metadata from decorators and class structure
|
|
6
|
+
*/
|
|
7
|
+
import { WorkflowAST } from '../types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Parse TypeScript workflow file
|
|
10
|
+
*/
|
|
11
|
+
export declare class TypeScriptParser {
|
|
12
|
+
private project;
|
|
13
|
+
constructor();
|
|
14
|
+
/**
|
|
15
|
+
* Parse TypeScript file
|
|
16
|
+
*/
|
|
17
|
+
parseFile(filePath: string): Promise<WorkflowAST>;
|
|
18
|
+
/**
|
|
19
|
+
* Parse TypeScript code string
|
|
20
|
+
*/
|
|
21
|
+
parseCode(code: string): Promise<WorkflowAST>;
|
|
22
|
+
/**
|
|
23
|
+
* Parse source file to AST
|
|
24
|
+
*/
|
|
25
|
+
private parseSourceFile;
|
|
26
|
+
/**
|
|
27
|
+
* Find class decorated with @workflow
|
|
28
|
+
*/
|
|
29
|
+
private findWorkflowClass;
|
|
30
|
+
/**
|
|
31
|
+
* Extract workflow metadata from @workflow decorator
|
|
32
|
+
*/
|
|
33
|
+
private extractWorkflowMetadata;
|
|
34
|
+
/**
|
|
35
|
+
* Extract nodes from class properties with @node decorator
|
|
36
|
+
*/
|
|
37
|
+
private extractNodes;
|
|
38
|
+
/**
|
|
39
|
+
* Extract connections from @links method
|
|
40
|
+
*/
|
|
41
|
+
private extractConnections;
|
|
42
|
+
/**
|
|
43
|
+
* Extract AI dependencies from .uses() calls in @links method
|
|
44
|
+
*
|
|
45
|
+
* Example:
|
|
46
|
+
* this.AgentIa.uses({
|
|
47
|
+
* ai_languageModel: this.OpenaiChatModel.output,
|
|
48
|
+
* ai_memory: this.Mmoire.output,
|
|
49
|
+
* ai_tool: [this.Tool1.output, this.Tool2.output]
|
|
50
|
+
* });
|
|
51
|
+
*/
|
|
52
|
+
private extractAIDependencies;
|
|
53
|
+
/**
|
|
54
|
+
* Parse AI dependencies object from .uses() call
|
|
55
|
+
*
|
|
56
|
+
* Input: "ai_languageModel: this.Model.output, ai_memory: this.Memory.output"
|
|
57
|
+
* Output: { ai_languageModel: "Model", ai_memory: "Memory" }
|
|
58
|
+
*/
|
|
59
|
+
private parseAIDependencies;
|
|
60
|
+
/**
|
|
61
|
+
* Split string by commas, but not inside brackets
|
|
62
|
+
*/
|
|
63
|
+
private splitByTopLevelCommas;
|
|
64
|
+
/**
|
|
65
|
+
* Parse tool array
|
|
66
|
+
*
|
|
67
|
+
* Input: "[this.Tool1.output, this.Tool2.output]"
|
|
68
|
+
* Output: ["Tool1", "Tool2"]
|
|
69
|
+
*/
|
|
70
|
+
private parseToolArray;
|
|
71
|
+
/**
|
|
72
|
+
* Parse a connection statement
|
|
73
|
+
*
|
|
74
|
+
* Examples:
|
|
75
|
+
* this.ScheduleTrigger.out(0).to(this.HttpRequest.in(0));
|
|
76
|
+
* this.GithubCheck.error().to(this.CreateBranch.in(0));
|
|
77
|
+
*/
|
|
78
|
+
private parseConnectionStatement;
|
|
79
|
+
/**
|
|
80
|
+
* Parse object literal string to object
|
|
81
|
+
*
|
|
82
|
+
* Uses Function constructor for safe eval of object literals
|
|
83
|
+
* This is safe because we only parse our own generated code
|
|
84
|
+
*/
|
|
85
|
+
private parseObjectLiteral;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=typescript-parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typescript-parser.d.ts","sourceRoot":"","sources":["../../src/compiler/typescript-parser.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,WAAW,EAA4C,MAAM,aAAa,CAAC;AAEpF;;GAEG;AACH,qBAAa,gBAAgB;IACzB,OAAO,CAAC,OAAO,CAAU;;IAazB;;OAEG;IACG,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAKvD;;OAEG;IACG,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAKnD;;OAEG;IACH,OAAO,CAAC,eAAe;IA2BvB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAgBzB;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAwB/B;;OAEG;IACH,OAAO,CAAC,YAAY;IA2CpB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAiD1B;;;;;;;;;OASG;IACH,OAAO,CAAC,qBAAqB;IA2D7B;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IAoC3B;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAyB7B;;;;;OAKG;IACH,OAAO,CAAC,cAAc;IAmBtB;;;;;;OAMG;IACH,OAAO,CAAC,wBAAwB;IA2ChC;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;CAU7B"}
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript Parser
|
|
3
|
+
*
|
|
4
|
+
* Parses TypeScript workflow files using ts-morph
|
|
5
|
+
* Extracts metadata from decorators and class structure
|
|
6
|
+
*/
|
|
7
|
+
import { Project, SyntaxKind } from 'ts-morph';
|
|
8
|
+
/**
|
|
9
|
+
* Parse TypeScript workflow file
|
|
10
|
+
*/
|
|
11
|
+
export class TypeScriptParser {
|
|
12
|
+
project;
|
|
13
|
+
constructor() {
|
|
14
|
+
this.project = new Project({
|
|
15
|
+
compilerOptions: {
|
|
16
|
+
target: 99, // ESNext
|
|
17
|
+
module: 99, // ESNext
|
|
18
|
+
experimentalDecorators: true,
|
|
19
|
+
emitDecoratorMetadata: true
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Parse TypeScript file
|
|
25
|
+
*/
|
|
26
|
+
async parseFile(filePath) {
|
|
27
|
+
const sourceFile = this.project.addSourceFileAtPath(filePath);
|
|
28
|
+
return this.parseSourceFile(sourceFile);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Parse TypeScript code string
|
|
32
|
+
*/
|
|
33
|
+
async parseCode(code) {
|
|
34
|
+
const sourceFile = this.project.createSourceFile('temp.ts', code, { overwrite: true });
|
|
35
|
+
return this.parseSourceFile(sourceFile);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Parse source file to AST
|
|
39
|
+
*/
|
|
40
|
+
parseSourceFile(sourceFile) {
|
|
41
|
+
// Find class with @workflow decorator
|
|
42
|
+
const workflowClass = this.findWorkflowClass(sourceFile);
|
|
43
|
+
if (!workflowClass) {
|
|
44
|
+
throw new Error('No class with @workflow decorator found in file');
|
|
45
|
+
}
|
|
46
|
+
// Extract workflow metadata
|
|
47
|
+
const metadata = this.extractWorkflowMetadata(workflowClass);
|
|
48
|
+
// Extract nodes
|
|
49
|
+
const nodes = this.extractNodes(workflowClass);
|
|
50
|
+
// Extract connections
|
|
51
|
+
const connections = this.extractConnections(workflowClass);
|
|
52
|
+
// Extract AI dependencies and add them to nodes
|
|
53
|
+
this.extractAIDependencies(workflowClass, nodes);
|
|
54
|
+
return {
|
|
55
|
+
metadata,
|
|
56
|
+
nodes,
|
|
57
|
+
connections
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Find class decorated with @workflow
|
|
62
|
+
*/
|
|
63
|
+
findWorkflowClass(sourceFile) {
|
|
64
|
+
const classes = sourceFile.getClasses();
|
|
65
|
+
for (const cls of classes) {
|
|
66
|
+
const decorators = cls.getDecorators();
|
|
67
|
+
for (const decorator of decorators) {
|
|
68
|
+
const decoratorName = decorator.getName();
|
|
69
|
+
if (decoratorName === 'workflow') {
|
|
70
|
+
return cls;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Extract workflow metadata from @workflow decorator
|
|
78
|
+
*/
|
|
79
|
+
extractWorkflowMetadata(workflowClass) {
|
|
80
|
+
const decorator = workflowClass.getDecorator('workflow');
|
|
81
|
+
if (!decorator) {
|
|
82
|
+
throw new Error('Class missing @workflow decorator');
|
|
83
|
+
}
|
|
84
|
+
// Get decorator arguments
|
|
85
|
+
const args = decorator.getArguments();
|
|
86
|
+
if (args.length === 0) {
|
|
87
|
+
throw new Error('@workflow decorator missing metadata argument');
|
|
88
|
+
}
|
|
89
|
+
// Parse object literal argument
|
|
90
|
+
const metadataArg = args[0];
|
|
91
|
+
const metadataText = metadataArg.getText();
|
|
92
|
+
// Use eval in a safe context to parse the object literal
|
|
93
|
+
// This is safe because we're only parsing our own generated code
|
|
94
|
+
const metadata = this.parseObjectLiteral(metadataText);
|
|
95
|
+
return metadata;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Extract nodes from class properties with @node decorator
|
|
99
|
+
*/
|
|
100
|
+
extractNodes(workflowClass) {
|
|
101
|
+
const nodes = [];
|
|
102
|
+
const properties = workflowClass.getProperties();
|
|
103
|
+
for (const prop of properties) {
|
|
104
|
+
const decorator = prop.getDecorator('node');
|
|
105
|
+
if (!decorator) {
|
|
106
|
+
continue; // Skip properties without @node decorator
|
|
107
|
+
}
|
|
108
|
+
// Extract node metadata from decorator
|
|
109
|
+
const args = decorator.getArguments();
|
|
110
|
+
if (args.length === 0) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const metadataText = args[0].getText();
|
|
114
|
+
const metadata = this.parseObjectLiteral(metadataText);
|
|
115
|
+
// Extract property name
|
|
116
|
+
const propertyName = prop.getName();
|
|
117
|
+
// Extract parameters from property initializer
|
|
118
|
+
const initializer = prop.getInitializer();
|
|
119
|
+
const parameters = initializer ? this.parseObjectLiteral(initializer.getText()) : {};
|
|
120
|
+
nodes.push({
|
|
121
|
+
propertyName,
|
|
122
|
+
displayName: metadata.name,
|
|
123
|
+
type: metadata.type,
|
|
124
|
+
version: metadata.version,
|
|
125
|
+
position: metadata.position || [0, 0],
|
|
126
|
+
credentials: metadata.credentials,
|
|
127
|
+
onError: metadata.onError,
|
|
128
|
+
parameters
|
|
129
|
+
// aiDependencies will be added by extractAIDependencies()
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return nodes;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Extract connections from @links method
|
|
136
|
+
*/
|
|
137
|
+
extractConnections(workflowClass) {
|
|
138
|
+
const connections = [];
|
|
139
|
+
// Find method with @links decorator
|
|
140
|
+
const methods = workflowClass.getMethods();
|
|
141
|
+
let linksMethod = null;
|
|
142
|
+
for (const method of methods) {
|
|
143
|
+
const decorator = method.getDecorator('links');
|
|
144
|
+
if (decorator) {
|
|
145
|
+
linksMethod = method;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (!linksMethod) {
|
|
150
|
+
return connections; // No connections defined
|
|
151
|
+
}
|
|
152
|
+
// Parse method body to extract connections
|
|
153
|
+
const body = linksMethod.getBody();
|
|
154
|
+
if (!body || !body.isKind(SyntaxKind.Block)) {
|
|
155
|
+
return connections;
|
|
156
|
+
}
|
|
157
|
+
// Get all statements in the method
|
|
158
|
+
const statements = body.getStatements();
|
|
159
|
+
for (const statement of statements) {
|
|
160
|
+
const text = statement.getText();
|
|
161
|
+
// Parse connection statements
|
|
162
|
+
// Format: this.NodeA.out(0).to(this.NodeB.in(0));
|
|
163
|
+
// Format: this.NodeA.error().to(this.NodeB.in(0));
|
|
164
|
+
// Skip .uses() calls (handled by extractAIDependencies)
|
|
165
|
+
if (text.includes('.uses(')) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const connection = this.parseConnectionStatement(text);
|
|
169
|
+
if (connection) {
|
|
170
|
+
connections.push(connection);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return connections;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Extract AI dependencies from .uses() calls in @links method
|
|
177
|
+
*
|
|
178
|
+
* Example:
|
|
179
|
+
* this.AgentIa.uses({
|
|
180
|
+
* ai_languageModel: this.OpenaiChatModel.output,
|
|
181
|
+
* ai_memory: this.Mmoire.output,
|
|
182
|
+
* ai_tool: [this.Tool1.output, this.Tool2.output]
|
|
183
|
+
* });
|
|
184
|
+
*/
|
|
185
|
+
extractAIDependencies(workflowClass, nodes) {
|
|
186
|
+
// Find method with @links decorator
|
|
187
|
+
const methods = workflowClass.getMethods();
|
|
188
|
+
let linksMethod = null;
|
|
189
|
+
for (const method of methods) {
|
|
190
|
+
const decorator = method.getDecorator('links');
|
|
191
|
+
if (decorator) {
|
|
192
|
+
linksMethod = method;
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (!linksMethod) {
|
|
197
|
+
return; // No links method
|
|
198
|
+
}
|
|
199
|
+
// Parse method body
|
|
200
|
+
const body = linksMethod.getBody();
|
|
201
|
+
if (!body || !body.isKind(SyntaxKind.Block)) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const statements = body.getStatements();
|
|
205
|
+
for (const statement of statements) {
|
|
206
|
+
const text = statement.getText();
|
|
207
|
+
// Only process .uses() calls
|
|
208
|
+
if (!text.includes('.uses(')) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
// Parse: this.NodeName.uses({ ... });
|
|
212
|
+
const usesMatch = text.match(/this\.(\w+)\.uses\s*\(\s*\{([^}]+)\}\s*\)/);
|
|
213
|
+
if (!usesMatch) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const targetNodeProperty = usesMatch[1];
|
|
217
|
+
const depsObjectText = usesMatch[2];
|
|
218
|
+
// Find the corresponding node
|
|
219
|
+
const node = nodes.find(n => n.propertyName === targetNodeProperty);
|
|
220
|
+
if (!node) {
|
|
221
|
+
console.warn(`Warning: .uses() called on unknown node: ${targetNodeProperty}`);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
// Parse dependencies object
|
|
225
|
+
const aiDependencies = this.parseAIDependencies(depsObjectText);
|
|
226
|
+
// Add to node
|
|
227
|
+
if (Object.keys(aiDependencies).length > 0) {
|
|
228
|
+
node.aiDependencies = aiDependencies;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Parse AI dependencies object from .uses() call
|
|
234
|
+
*
|
|
235
|
+
* Input: "ai_languageModel: this.Model.output, ai_memory: this.Memory.output"
|
|
236
|
+
* Output: { ai_languageModel: "Model", ai_memory: "Memory" }
|
|
237
|
+
*/
|
|
238
|
+
parseAIDependencies(depsText) {
|
|
239
|
+
const result = {};
|
|
240
|
+
// Split by comma (but not inside brackets)
|
|
241
|
+
const entries = this.splitByTopLevelCommas(depsText);
|
|
242
|
+
for (const entry of entries) {
|
|
243
|
+
const trimmed = entry.trim();
|
|
244
|
+
if (!trimmed)
|
|
245
|
+
continue;
|
|
246
|
+
// Parse: key: value
|
|
247
|
+
const colonIndex = trimmed.indexOf(':');
|
|
248
|
+
if (colonIndex === -1)
|
|
249
|
+
continue;
|
|
250
|
+
const key = trimmed.substring(0, colonIndex).trim();
|
|
251
|
+
const value = trimmed.substring(colonIndex + 1).trim();
|
|
252
|
+
// Check if it's an array type (ai_tool or ai_document)
|
|
253
|
+
if ((key === 'ai_tool' || key === 'ai_document') && value.startsWith('[')) {
|
|
254
|
+
// Parse array: [this.Tool1.output, this.Tool2.output]
|
|
255
|
+
const itemNames = this.parseToolArray(value);
|
|
256
|
+
if (itemNames.length > 0) {
|
|
257
|
+
result[key] = itemNames;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
// Parse single reference: this.NodeName.output
|
|
262
|
+
const nodeMatch = value.match(/this\.(\w+)\.output/);
|
|
263
|
+
if (nodeMatch) {
|
|
264
|
+
result[key] = nodeMatch[1];
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Split string by commas, but not inside brackets
|
|
272
|
+
*/
|
|
273
|
+
splitByTopLevelCommas(text) {
|
|
274
|
+
const result = [];
|
|
275
|
+
let current = '';
|
|
276
|
+
let bracketDepth = 0;
|
|
277
|
+
for (const char of text) {
|
|
278
|
+
if (char === '[') {
|
|
279
|
+
bracketDepth++;
|
|
280
|
+
}
|
|
281
|
+
else if (char === ']') {
|
|
282
|
+
bracketDepth--;
|
|
283
|
+
}
|
|
284
|
+
else if (char === ',' && bracketDepth === 0) {
|
|
285
|
+
result.push(current);
|
|
286
|
+
current = '';
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
current += char;
|
|
290
|
+
}
|
|
291
|
+
if (current.trim()) {
|
|
292
|
+
result.push(current);
|
|
293
|
+
}
|
|
294
|
+
return result;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Parse tool array
|
|
298
|
+
*
|
|
299
|
+
* Input: "[this.Tool1.output, this.Tool2.output]"
|
|
300
|
+
* Output: ["Tool1", "Tool2"]
|
|
301
|
+
*/
|
|
302
|
+
parseToolArray(arrayText) {
|
|
303
|
+
const result = [];
|
|
304
|
+
// Remove brackets
|
|
305
|
+
const content = arrayText.replace(/^\[|\]$/g, '').trim();
|
|
306
|
+
// Split by comma
|
|
307
|
+
const items = content.split(',');
|
|
308
|
+
for (const item of items) {
|
|
309
|
+
const nodeMatch = item.trim().match(/this\.(\w+)\.output/);
|
|
310
|
+
if (nodeMatch) {
|
|
311
|
+
result.push(nodeMatch[1]);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return result;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Parse a connection statement
|
|
318
|
+
*
|
|
319
|
+
* Examples:
|
|
320
|
+
* this.ScheduleTrigger.out(0).to(this.HttpRequest.in(0));
|
|
321
|
+
* this.GithubCheck.error().to(this.CreateBranch.in(0));
|
|
322
|
+
*/
|
|
323
|
+
parseConnectionStatement(statement) {
|
|
324
|
+
// Remove whitespace and semicolon
|
|
325
|
+
const cleaned = statement.trim().replace(/;$/, '');
|
|
326
|
+
// Pattern: this.{fromNode}.{output}.to(this.{toNode}.in({input}))
|
|
327
|
+
const errorPattern = /this\.(\w+)\.error\(\)\.to\(this\.(\w+)\.in\((\d+)\)\)/;
|
|
328
|
+
const normalPattern = /this\.(\w+)\.out\((\d+)\)\.to\(this\.(\w+)\.in\((\d+)\)\)/;
|
|
329
|
+
// Try error pattern first
|
|
330
|
+
let match = cleaned.match(errorPattern);
|
|
331
|
+
if (match) {
|
|
332
|
+
return {
|
|
333
|
+
from: {
|
|
334
|
+
node: match[1],
|
|
335
|
+
output: 0,
|
|
336
|
+
isError: true
|
|
337
|
+
},
|
|
338
|
+
to: {
|
|
339
|
+
node: match[2],
|
|
340
|
+
input: parseInt(match[3])
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
// Try normal pattern
|
|
345
|
+
match = cleaned.match(normalPattern);
|
|
346
|
+
if (match) {
|
|
347
|
+
return {
|
|
348
|
+
from: {
|
|
349
|
+
node: match[1],
|
|
350
|
+
output: parseInt(match[2]),
|
|
351
|
+
isError: false
|
|
352
|
+
},
|
|
353
|
+
to: {
|
|
354
|
+
node: match[3],
|
|
355
|
+
input: parseInt(match[4])
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Parse object literal string to object
|
|
363
|
+
*
|
|
364
|
+
* Uses Function constructor for safe eval of object literals
|
|
365
|
+
* This is safe because we only parse our own generated code
|
|
366
|
+
*/
|
|
367
|
+
parseObjectLiteral(text) {
|
|
368
|
+
try {
|
|
369
|
+
// Wrap in parentheses and use Function constructor
|
|
370
|
+
const func = new Function(`return (${text})`);
|
|
371
|
+
return func();
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
console.error('Failed to parse object literal:', text);
|
|
375
|
+
throw new Error(`Failed to parse object literal: ${error}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
//# sourceMappingURL=typescript-parser.js.map
|