diff --git a/deno.json b/deno.json
index 98cdd89..e89e484 100644
--- a/deno.json
+++ b/deno.json
@@ -1,6 +1,6 @@
{
"name": "@goog/flow-lens",
- "version": "0.1.15",
+ "version": "0.1.16",
"license": "Apache",
"exports": "./src/main/main.ts",
"imports": {
diff --git a/src/main/flow_parser.ts b/src/main/flow_parser.ts
index 456fc6a..783cb37 100644
--- a/src/main/flow_parser.ts
+++ b/src/main/flow_parser.ts
@@ -687,7 +687,16 @@ function isLoop(node: flowTypes.FlowNode): node is flowTypes.FlowLoop {
function isRecordCreate(
node: flowTypes.FlowNode,
): node is flowTypes.FlowRecordCreate {
- return (node as flowTypes.FlowRecordCreate).inputReference !== undefined;
+ const recordCreate = node as flowTypes.FlowRecordCreate;
+ // Check for required FlowRecordCreate properties
+ const hasInputAssignments = recordCreate.inputAssignments !== undefined &&
+ Array.isArray(recordCreate.inputAssignments);
+ const hasObject = recordCreate.object !== undefined;
+ // FlowRecordUpdate has filters, FlowRecordCreate does not
+ const recordUpdate = node as flowTypes.FlowRecordUpdate;
+ const hasNoFilters = recordUpdate.filters === undefined;
+
+ return hasInputAssignments && hasObject && hasNoFilters;
}
function isRecordDelete(
@@ -705,7 +714,12 @@ function isRecordLookup(
function isRecordUpdate(
node: flowTypes.FlowNode,
): node is flowTypes.FlowRecordUpdate {
- return (node as flowTypes.FlowRecordUpdate).inputReference !== undefined;
+ const recordUpdate = node as flowTypes.FlowRecordUpdate;
+ return (
+ recordUpdate.inputAssignments !== undefined &&
+ recordUpdate.filters !== undefined &&
+ recordUpdate.object !== undefined
+ );
}
function isRecordRollback(
diff --git a/src/main/flow_types.ts b/src/main/flow_types.ts
index cfe5d62..fae5ca7 100644
--- a/src/main/flow_types.ts
+++ b/src/main/flow_types.ts
@@ -554,7 +554,7 @@ export interface FlowRecordCreate extends FlowNode {
connector?: FlowConnector;
faultConnector?: FlowConnector;
inputAssignments: FlowInputFieldAssignment[];
- inputReference: string;
+ inputReference?: string;
object: string;
storeOutputAutomatically?: boolean; // default: false, Available in API version 48.0 and later
}
@@ -598,7 +598,7 @@ export interface FlowRecordUpdate extends FlowNode {
faultConnector?: FlowConnector;
filters: FlowRecordFilter[];
inputAssignments: FlowInputFieldAssignment[]; // Assuming FlowInputFieldAssignment is a pre-existing interface
- inputReference: string;
+ inputReference?: string;
object: string;
}
diff --git a/src/test/flow_parser_test.ts b/src/test/flow_parser_test.ts
index e91e1fc..61d7c67 100644
--- a/src/test/flow_parser_test.ts
+++ b/src/test/flow_parser_test.ts
@@ -56,6 +56,14 @@ const TEST_FILES = {
GOLDENS_PATH,
"non_async_scheduled_path.flow-meta.xml",
),
+ recordCreateWithoutInputReference: join(
+ GOLDENS_PATH,
+ "record_create_without_input_reference.flow-meta.xml",
+ ),
+ recordCreateVsRecordUpdate: join(
+ GOLDENS_PATH,
+ "record_create_vs_record_update.flow-meta.xml",
+ ),
};
const NODE_NAMES = {
@@ -534,11 +542,73 @@ Deno.test("FlowParser", async (t) => {
},
);
+ await t.step("should handle single async path correctly", async () => {
+ systemUnderTest = new FlowParser(
+ Deno.readTextFileSync(TEST_FILES.asyncPathTest),
+ );
+
+ parsedFlow = await systemUnderTest.generateFlowDefinition();
+
+ assert(parsedFlow.transitions);
+ assertEquals(parsedFlow.transitions.length, 2);
+
+ // Main flow transition (no label)
+ assertEquals(parsedFlow.transitions[0], {
+ from: START_NODE_NAME,
+ to: "main_update",
+ fault: false,
+ label: undefined,
+ });
+
+ // Async path transition (with path type as label)
+ assertEquals(parsedFlow.transitions[1], {
+ from: START_NODE_NAME,
+ to: "async_update",
+ fault: false,
+ label: "AsyncAfterCommit",
+ });
+ });
+
+ await t.step("should handle multiple async paths correctly", async () => {
+ systemUnderTest = new FlowParser(
+ Deno.readTextFileSync(TEST_FILES.multipleAsyncPaths),
+ );
+
+ parsedFlow = await systemUnderTest.generateFlowDefinition();
+
+ assert(parsedFlow.transitions);
+ assertEquals(parsedFlow.transitions.length, 3);
+
+ // Main flow transition (no label)
+ assertEquals(parsedFlow.transitions[0], {
+ from: START_NODE_NAME,
+ to: "main_update",
+ fault: false,
+ label: undefined,
+ });
+
+ // First async path transition
+ assertEquals(parsedFlow.transitions[1], {
+ from: START_NODE_NAME,
+ to: "async_update_1",
+ fault: false,
+ label: "AsyncAfterCommit",
+ });
+
+ // Second async path transition
+ assertEquals(parsedFlow.transitions[2], {
+ from: START_NODE_NAME,
+ to: "async_update_2",
+ fault: false,
+ label: "AsyncAfterCommit",
+ });
+ });
+
await t.step(
- "should handle single async path correctly",
+ "should not label non-async scheduled paths as asynchronous",
async () => {
systemUnderTest = new FlowParser(
- Deno.readTextFileSync(TEST_FILES.asyncPathTest),
+ Deno.readTextFileSync(TEST_FILES.nonAsyncScheduledPath),
);
parsedFlow = await systemUnderTest.generateFlowDefinition();
@@ -554,81 +624,139 @@ Deno.test("FlowParser", async (t) => {
label: undefined,
});
- // Async path transition (with path type as label)
+ // Non-async scheduled path transition (with path type as label)
assertEquals(parsedFlow.transitions[1], {
from: START_NODE_NAME,
- to: "async_update",
+ to: "scheduled_update",
fault: false,
- label: "AsyncAfterCommit",
+ label: "RecordField",
});
},
);
await t.step(
- "should handle multiple async paths correctly",
+ "should include fault connectors for recordCreates nodes without inputReference",
async () => {
systemUnderTest = new FlowParser(
- Deno.readTextFileSync(TEST_FILES.multipleAsyncPaths),
+ Deno.readTextFileSync(TEST_FILES.recordCreateWithoutInputReference),
);
parsedFlow = await systemUnderTest.generateFlowDefinition();
+ assert(parsedFlow);
+ assert(parsedFlow.recordCreates);
+ assertEquals(parsedFlow.recordCreates.length, 1);
+
+ const recordCreate = parsedFlow.recordCreates[0];
+ assertEquals(recordCreate.name, "create_property");
+ assertEquals(recordCreate.object, "Property__c");
+ // Verify that inputAssignments is present (required for identification)
+ assert(recordCreate.inputAssignments);
+ assertEquals(recordCreate.inputAssignments.length, 2);
+ // Verify that inputReference is optional and can be undefined
+ assertEquals(recordCreate.inputReference, undefined);
+ // Verify that fault connector is present and parsed correctly
+ assert(recordCreate.faultConnector);
+ assertEquals(
+ recordCreate.faultConnector.targetReference,
+ "error_creating_records",
+ );
+ // Verify that normal connector is also present
+ assert(recordCreate.connector);
+ assertEquals(recordCreate.connector.targetReference, "success_screen");
+
+ // Verify that transitions include both normal and fault paths
assert(parsedFlow.transitions);
assertEquals(parsedFlow.transitions.length, 3);
- // Main flow transition (no label)
+ // Start node to create_property
assertEquals(parsedFlow.transitions[0], {
from: START_NODE_NAME,
- to: "main_update",
+ to: "create_property",
fault: false,
label: undefined,
});
- // First async path transition
+ // Normal path: create_property to success_screen
assertEquals(parsedFlow.transitions[1], {
- from: START_NODE_NAME,
- to: "async_update_1",
+ from: "create_property",
+ to: "success_screen",
fault: false,
- label: "AsyncAfterCommit",
+ label: undefined,
});
- // Second async path transition
+ // Fault path: create_property to error_creating_records
assertEquals(parsedFlow.transitions[2], {
- from: START_NODE_NAME,
- to: "async_update_2",
- fault: false,
- label: "AsyncAfterCommit",
+ from: "create_property",
+ to: "error_creating_records",
+ fault: true,
+ label: "Fault",
});
},
);
await t.step(
- "should not label non-async scheduled paths as asynchronous",
+ "should still work correctly for recordCreates nodes with inputReference (backward compatibility)",
async () => {
systemUnderTest = new FlowParser(
- Deno.readTextFileSync(TEST_FILES.nonAsyncScheduledPath),
+ Deno.readTextFileSync(TEST_FILES.sample),
);
parsedFlow = await systemUnderTest.generateFlowDefinition();
- assert(parsedFlow.transitions);
- assertEquals(parsedFlow.transitions.length, 2);
+ assert(parsedFlow);
+ assert(parsedFlow.recordCreates);
+ assertEquals(parsedFlow.recordCreates.length, 1);
- // Main flow transition (no label)
- assertEquals(parsedFlow.transitions[0], {
- from: START_NODE_NAME,
- to: "main_update",
- fault: false,
- label: undefined,
- });
+ const recordCreate = parsedFlow.recordCreates[0];
+ assertEquals(recordCreate.name, "Insert_Tag");
+ // Verify that inputReference is present (traditional approach)
+ assertEquals(recordCreate.inputReference, "tagToInsert");
+ // Verify that fault connector is still correctly parsed and included
+ assert(recordCreate.faultConnector);
+ assertEquals(
+ recordCreate.faultConnector.targetReference,
+ "Add_Issue_Inserting_Tag_Record_Error",
+ );
- // Non-async scheduled path transition (with path type as label)
- assertEquals(parsedFlow.transitions[1], {
- from: START_NODE_NAME,
- to: "scheduled_update",
- fault: false,
- label: "RecordField",
- });
+ // Verify that the fault transition is included in the transitions array
+ const faultTransition = parsedFlow.transitions?.find(
+ (t) => t.from === "Insert_Tag" && t.fault === true,
+ );
+ assert(faultTransition);
+ assertEquals(faultTransition.to, "Add_Issue_Inserting_Tag_Record_Error");
+ assertEquals(faultTransition.label, "Fault");
+ },
+ );
+
+ await t.step(
+ "should correctly distinguish recordCreates from recordUpdates",
+ async () => {
+ systemUnderTest = new FlowParser(
+ Deno.readTextFileSync(TEST_FILES.recordCreateVsRecordUpdate),
+ );
+ parsedFlow = await systemUnderTest.generateFlowDefinition();
+
+ assert(parsedFlow);
+
+ // Verify recordCreates is identified correctly (has inputAssignments but no filters)
+ assert(parsedFlow.recordCreates);
+ assertEquals(parsedFlow.recordCreates.length, 1);
+ assertEquals(parsedFlow.recordCreates[0].name, "create_record");
+ assert(parsedFlow.recordCreates[0].inputAssignments);
+ // recordCreates should not have filters
+ assertEquals(
+ (parsedFlow.recordCreates[0] as flowTypes.FlowRecordUpdate).filters,
+ undefined,
+ );
+
+ // Verify recordUpdates is identified correctly (has both inputAssignments and filters)
+ assert(parsedFlow.recordUpdates);
+ assertEquals(parsedFlow.recordUpdates.length, 1);
+ assertEquals(parsedFlow.recordUpdates[0].name, "update_record");
+ assert(parsedFlow.recordUpdates[0].inputAssignments);
+ assert(parsedFlow.recordUpdates[0].filters);
+ assertEquals(parsedFlow.recordUpdates[0].filters.length, 1);
},
);
});
diff --git a/src/test/goldens/multiple_elements.flow-meta.xml b/src/test/goldens/multiple_elements.flow-meta.xml
index 7ed6600..b66476e 100644
--- a/src/test/goldens/multiple_elements.flow-meta.xml
+++ b/src/test/goldens/multiple_elements.flow-meta.xml
@@ -1,6 +1,6 @@
+
+
+ 64.0
+
+ Flow
+
+ 0
+ 0
+
+ create_record
+
+
+
+ create_record
+
+ 100
+ 100
+
+ update_record
+
+
+ Name
+
+ Test
+
+
+
+
+
+ update_record
+
+ 200
+ 200
+
+ Id
+ EqualTo
+
+ create_record
+
+
+
+ Status__c
+
+ Active
+
+
+
+
+
diff --git a/src/test/goldens/record_create_without_input_reference.flow-meta.xml b/src/test/goldens/record_create_without_input_reference.flow-meta.xml
new file mode 100644
index 0000000..7e67fe9
--- /dev/null
+++ b/src/test/goldens/record_create_without_input_reference.flow-meta.xml
@@ -0,0 +1,68 @@
+
+
+
+
+ 64.0
+
+ Flow
+
+ 0
+ 0
+
+ create_property
+
+
+
+ create_property
+
+ 666
+ 638
+
+ success_screen
+
+
+ error_creating_records
+
+
+ Name
+
+ Test Property
+
+
+
+ Status__c
+
+ Available
+
+
+
+
+ true
+
+
+ success_screen
+
+ 666
+ 758
+
+
+ error_creating_records
+
+ 1458
+ 758
+
+
diff --git a/src/test/goldens/single_elements.flow-meta.xml b/src/test/goldens/single_elements.flow-meta.xml
index 0a93a7b..65548e7 100644
--- a/src/test/goldens/single_elements.flow-meta.xml
+++ b/src/test/goldens/single_elements.flow-meta.xml
@@ -43,6 +43,8 @@
myRecordCreate
+
+
myRecordDelete