Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@goog/flow-lens",
"version": "0.1.15",
"version": "0.1.16",
"license": "Apache",
"exports": "./src/main/main.ts",
"imports": {
Expand Down
18 changes: 16 additions & 2 deletions src/main/flow_parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions src/main/flow_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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;
}

Expand Down
200 changes: 164 additions & 36 deletions src/test/flow_parser_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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();
Expand All @@ -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);
},
);
});
6 changes: 5 additions & 1 deletion src/test/goldens/multiple_elements.flow-meta.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2024 Google LLC
Copyright 2026 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -61,9 +61,13 @@
</orchestratedStages>
<recordCreates>
<name>myRecordCreate</name>
<object>Account</object>
<inputAssignments></inputAssignments>
</recordCreates>
<recordCreates>
<name>myRecordCreate2</name>
<object>Account</object>
<inputAssignments></inputAssignments>
</recordCreates>
<recordDeletes>
<name>myRecordDelete</name>
Expand Down
Loading
Loading