diff --git a/.eslintrc.json b/.eslintrc.json index e18e8d4..444452e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -26,6 +26,7 @@ "import/prefer-default-export": "warn", "import/no-extraneous-dependencies": "off", "@typescript-eslint/no-use-before-define": "off", - "unicorn/filename-case": "off" + "unicorn/filename-case": "off", + "max-classes-per-file": "off" } } diff --git a/messages/messages.json b/messages/messages.json index 6eb442b..7d3736a 100644 --- a/messages/messages.json +++ b/messages/messages.json @@ -1,6 +1,6 @@ { "errorParamNotFound": "Flow API name is missing.", - "errorUnsupportedFlow": "Currently, only process is supported. the other type of processes will be available soon.", + "errorUnsupportedFlow": "The flow is not supported now.", "commandDescription": "generate pdf document", "nameFlagDescription": "developer name of flow", "localeFlagDescription": "locale of the document (en or ja)", diff --git a/src/commands/flowdoc/docx/generate.ts b/src/commands/flowdoc/docx/generate.ts index 5eff976..a8ea996 100644 --- a/src/commands/flowdoc/docx/generate.ts +++ b/src/commands/flowdoc/docx/generate.ts @@ -3,14 +3,18 @@ import { Messages, SfdxError } from '@salesforce/core'; import * as fs from 'fs-extra'; import * as docx from 'docx'; -import { Flow } from '../../../types/flow'; -import FlowParser from '../../../lib/flowParser'; -import buildDocxContent from '../../../lib/docx/docxBuilder'; +import { Flow } from '../../../types/metadata/flow'; +import { + ReadableProcessMetadataConverter, + ReadableFlowMetadataConverter, +} from '../../../lib/converter/metadataConverter'; +import { isSupported, isProcess } from '../../../lib/util/flowUtils'; +import DocxBuilder from '../../../lib/docx/docxBuilder'; +import { API_VERSION } from '../../../lib/converter/helper/constants'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('sfdx-flowdoc-plugin', 'messages'); -const API_VERSION = '48.0'; export default class Generate extends SfdxCommand { public static description = messages.getMessage('commandDescription'); @@ -45,21 +49,22 @@ export default class Generate extends SfdxCommand { const conn = this.org.getConnection(); conn.setApiVersion(API_VERSION); - const flow = await conn.metadata.read('Flow', this.args.file); - + const flow = ((await conn.metadata.read('Flow', this.args.file)) as unknown) as Flow; if (Object.keys(flow).length === 0) { this.ux.stopSpinner('failed.'); throw new SfdxError(messages.getMessage('errorFlowNotFound')); } - const fp = new FlowParser((flow as unknown) as Flow, this.args.file); - if (!fp.isSupportedFlow()) { + + if (!isSupported(flow)) { this.ux.stopSpinner('failed.'); throw new SfdxError(messages.getMessage('errorUnsupportedFlow')); } this.ux.stopSpinner(); - const hrDoc = fp.createReadableProcess(); - const doc = buildDocxContent(hrDoc, this.flags.locale); + const docxBuilder = new DocxBuilder(this.flags.locale); + const doc = isProcess(flow) + ? new ReadableProcessMetadataConverter(flow, this.args.file).accept(docxBuilder) + : new ReadableFlowMetadataConverter(flow, this.args.file).accept(docxBuilder); const outdir = this.flags.outdir ? this.flags.outdir : '.'; await fs.ensureDir(outdir); @@ -68,8 +73,7 @@ export default class Generate extends SfdxCommand { fs.writeFileSync(`${outdir}/${targetPath}`, buffer); }); - const label: string = fp.getLabel(); - this.ux.log(`Documentation of '${label}' flow is successfully generated.`); + this.ux.log(`Documentation of '${flow.label}' flow is successfully generated.`); return flow; } diff --git a/src/commands/flowdoc/json/display.ts b/src/commands/flowdoc/json/display.ts index f57ec21..77ccf60 100644 --- a/src/commands/flowdoc/json/display.ts +++ b/src/commands/flowdoc/json/display.ts @@ -1,14 +1,18 @@ import { flags, SfdxCommand } from '@salesforce/command'; import { Messages, SfdxError } from '@salesforce/core'; -import { Flow } from '../../../types/flow'; -import FlowParser from '../../../lib/flowParser'; -import buildLocalizedJson from '../../../lib/json/jsonBuilder'; +import { Flow } from '../../../types/metadata/flow'; +import { isSupported, isProcess } from '../../../lib/util/flowUtils'; +import JsonBuilder from '../../../lib/json/jsonBuilder'; +import { + ReadableProcessMetadataConverter, + ReadableFlowMetadataConverter, +} from '../../../lib/converter/metadataConverter'; +import { API_VERSION } from '../../../lib/converter/helper/constants'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('sfdx-flowdoc-plugin', 'messages'); -const API_VERSION = '48.0'; export default class Display extends SfdxCommand { public static description = messages.getMessage('commandDescription'); @@ -41,22 +45,23 @@ export default class Display extends SfdxCommand { const conn = this.org.getConnection(); conn.setApiVersion(API_VERSION); - const flow = await conn.metadata.read('Flow', this.args.file); - + const flow = ((await conn.metadata.read('Flow', this.args.file)) as unknown) as Flow; if (Object.keys(flow).length === 0) { this.ux.stopSpinner('failed.'); throw new SfdxError(messages.getMessage('errorFlowNotFound')); } - const fp = new FlowParser((flow as unknown) as Flow, this.args.file); - if (!fp.isSupportedFlow()) { + + if (!isSupported(flow)) { this.ux.stopSpinner('failed.'); throw new SfdxError(messages.getMessage('errorUnsupportedFlow')); } this.ux.stopSpinner(); - const readableFlow = fp.createReadableProcess(); - const localizedJson = buildLocalizedJson(readableFlow, this.flags.locale); - this.ux.log(JSON.stringify(localizedJson, null, ' ')); + const jsonBuilder = new JsonBuilder(this.flags.locale); + const json = isProcess(flow) + ? new ReadableProcessMetadataConverter(flow, this.args.file).accept(jsonBuilder) + : new ReadableFlowMetadataConverter(flow, this.args.file).accept(jsonBuilder); + this.ux.log(JSON.stringify(json, null, ' ')); return flow; } diff --git a/src/commands/flowdoc/json/generate.ts b/src/commands/flowdoc/json/generate.ts index 1e12388..a6b4c08 100644 --- a/src/commands/flowdoc/json/generate.ts +++ b/src/commands/flowdoc/json/generate.ts @@ -2,14 +2,18 @@ import { flags, SfdxCommand } from '@salesforce/command'; import { Messages, SfdxError } from '@salesforce/core'; import * as fs from 'fs-extra'; -import { Flow } from '../../../types/flow'; -import FlowParser from '../../../lib/flowParser'; -import buildLocalizedJson from '../../../lib/json/jsonBuilder'; +import { Flow } from '../../../types/metadata/flow'; +import { isSupported, isProcess } from '../../../lib/util/flowUtils'; +import JsonBuilder from '../../../lib/json/jsonBuilder'; +import { + ReadableProcessMetadataConverter, + ReadableFlowMetadataConverter, +} from '../../../lib/converter/metadataConverter'; +import { API_VERSION } from '../../../lib/converter/helper/constants'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('sfdx-flowdoc-plugin', 'messages'); -const API_VERSION = '48.0'; export default class Generate extends SfdxCommand { public static description = messages.getMessage('commandDescription'); @@ -45,30 +49,30 @@ export default class Generate extends SfdxCommand { const conn = this.org.getConnection(); conn.setApiVersion(API_VERSION); - const flow = await conn.metadata.read('Flow', this.args.file); - + const flow = ((await conn.metadata.read('Flow', this.args.file)) as unknown) as Flow; if (Object.keys(flow).length === 0) { this.ux.stopSpinner('failed.'); throw new SfdxError(messages.getMessage('errorFlowNotFound')); } - const fp = new FlowParser((flow as unknown) as Flow, this.args.file); - if (!fp.isSupportedFlow()) { + + if (!isSupported(flow)) { this.ux.stopSpinner('failed.'); throw new SfdxError(messages.getMessage('errorUnsupportedFlow')); } this.ux.stopSpinner(); - const readableFlow = fp.createReadableProcess(); - const localizedJson = buildLocalizedJson(readableFlow, this.flags.locale); + const jsonBuilder = new JsonBuilder(this.flags.locale); + const json = isProcess(flow) + ? new ReadableProcessMetadataConverter(flow, this.args.file).accept(jsonBuilder) + : new ReadableFlowMetadataConverter(flow, this.args.file).accept(jsonBuilder); const targetPath = `${this.args.file}.json`; const outdir = this.flags.outdir ? this.flags.outdir : '.'; await fs.ensureDir(outdir); - fs.writeFileSync(`${outdir}/${targetPath}`, JSON.stringify(localizedJson, null, ' ')); + fs.writeFileSync(`${outdir}/${targetPath}`, JSON.stringify(json, null, ' ')); - const label: string = fp.getLabel(); - this.ux.log(`Documentation of '${label}' flow is successfully generated.`); + this.ux.log(`Documentation of '${flow.label}' flow is successfully generated.`); return flow; } diff --git a/src/commands/flowdoc/pdf/generate.ts b/src/commands/flowdoc/pdf/generate.ts index 1abefb3..5caa3db 100644 --- a/src/commands/flowdoc/pdf/generate.ts +++ b/src/commands/flowdoc/pdf/generate.ts @@ -1,17 +1,21 @@ import { flags, SfdxCommand } from '@salesforce/command'; import { Messages, SfdxError } from '@salesforce/core'; import * as fs from 'fs-extra'; -import { Flow } from '../../../types/flow'; -import FlowParser from '../../../lib/flowParser'; +import { Flow } from '../../../types/metadata/flow'; import fonts from '../../../style/font'; -import buildPdfContent from '../../../lib/pdf/pdfBuilder'; +import PdfBuilder from '../../../lib/pdf/pdfBuilder'; +import { + ReadableProcessMetadataConverter, + ReadableFlowMetadataConverter, +} from '../../../lib/converter/metadataConverter'; +import { isSupported, isProcess } from '../../../lib/util/flowUtils'; +import { API_VERSION } from '../../../lib/converter/helper/constants'; const Pdf = require('pdfmake'); Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('sfdx-flowdoc-plugin', 'messages'); -const API_VERSION = '48.0'; export default class Generate extends SfdxCommand { public static description = messages.getMessage('commandDescription'); @@ -44,20 +48,22 @@ export default class Generate extends SfdxCommand { const conn = this.org.getConnection(); conn.setApiVersion(API_VERSION); - const flow = await conn.metadata.read('Flow', this.args.file); + const flow = ((await conn.metadata.read('Flow', this.args.file)) as unknown) as Flow; if (Object.keys(flow).length === 0) { this.ux.stopSpinner('failed.'); throw new SfdxError(messages.getMessage('errorFlowNotFound')); } - const fp = new FlowParser((flow as unknown) as Flow, this.args.file); - if (!fp.isSupportedFlow()) { + + if (!isSupported(flow)) { this.ux.stopSpinner('failed.'); throw new SfdxError(messages.getMessage('errorUnsupportedFlow')); } this.ux.stopSpinner(); - const hrDoc = fp.createReadableProcess(); - const docDefinition = buildPdfContent(hrDoc, this.flags.locale); + const pdfBuilder = new PdfBuilder(this.flags.locale); + const docDefinition = isProcess(flow) + ? new ReadableProcessMetadataConverter(flow, this.args.file).accept(pdfBuilder) + : new ReadableFlowMetadataConverter(flow, this.args.file).accept(pdfBuilder); const printer = new Pdf(fonts); const pdfDoc = printer.createPdfKitDocument(docDefinition); @@ -67,8 +73,7 @@ export default class Generate extends SfdxCommand { const targetPath = `${this.args.file}.pdf`; pdfDoc.pipe(fs.createWriteStream(`${outdir}/${targetPath}`)); pdfDoc.end(); - const label: string = fp.getLabel(); - this.ux.log(`Documentation of '${label}' flow is successfully generated.`); + this.ux.log(`Documentation of '${flow.label}' flow is successfully generated.`); return flow; } diff --git a/src/config/locale/en.json b/src/config/locale/en.json index 1156459..ac3b889 100644 --- a/src/config/locale/en.json +++ b/src/config/locale/en.json @@ -8,6 +8,18 @@ "WHEN_THE_PROCESS_STARTS": "When the process starts", "WHEN_A_RECORD_IS_CREATED_OR_EDITED": "When a record is created or edited", "ONLY_WHEN_A_RECORD_IS_CREATED": "Only when a record is created", + "FLOW_TRIGGER_TYPE": "Flow Type", + "FLOW_TRIGGER_AUTO_LAUNCHED": "Auto Launched Flow", + "FLOW_TRIGGER_RECORD": "Record Change Flow", + "FLOW_TRIGGER_SCHEDULED": "Scheduled Flow", + "FLOW_TRIGGER_PLATFORM_EVENT": "Platform Event Flow", + "HEADER_FLOW_TRIGGER_RECORD": "Trigger the Flow When", + "FROW_TRIGGER_RECORD_CREATE_ONLY": "A record is created", + "FROW_TRIGGER_RECORD_UPDATE_ONLY": "A record is updated", + "FROW_TRIGGER_RECORD_CREATE_OR_UPDATE": "A record is created or updated", + "HEADER_FLOW_TRIGGER_RECORD_TYPE": "Run the Flow", + "FLOW_TRIGGER_BEFORE": "Before the record is saved", + "FLOW_TRIGGER_AFTER": "After the record is saved", "ACTION_GROUP": "Action Group", "CONDITION_NAME": "Condition Name", "CRITERIA_FOR_EXECUTING_ACTIONS": "Criteria for Execution Actions", diff --git a/src/config/locale/ja.json b/src/config/locale/ja.json index 4eded57..a2b07ee 100644 --- a/src/config/locale/ja.json +++ b/src/config/locale/ja.json @@ -8,6 +8,18 @@ "WHEN_THE_PROCESS_STARTS": "プロセスを開始", "WHEN_A_RECORD_IS_CREATED_OR_EDITED": "レコードを作成したときのみ", "ONLY_WHEN_A_RECORD_IS_CREATED": "レコードを作成または編集したとき", + "FLOW_TRIGGER_TYPE": "フローの種別", + "FLOW_TRIGGER_AUTOLAUNCHED": "自動起動", + "FLOW_TRIGGER_RECORD": "レコード変更フロー", + "FLOW_TRIGGER_SCHEDULED": "スケジュール済みフロー", + "FLOW_TRIGGER_PLATFORM_EVENT": "プラットフォームイベントフロー", + "HEADER_FLOW_TRIGGER_RECORD": "フローをトリガする条件", + "FROW_TRIGGER_RECORD_CREATE_ONLY": "レコードの作成時", + "FROW_TRIGGER_RECORD_UPDATE_ONLY": "レコードの更新時", + "FROW_TRIGGER_RECORD_CREATE_OR_UPDATE": "レコードの作成または更新時", + "HEADER_FLOW_TRIGGER_RECORD_TYPE": "フローを実行", + "FLOW_TRIGGER_BEFORE": "レコードが保存される前", + "FLOW_TRIGGER_AFTER": "レコードが保存された後", "ACTION_GROUP": "アクショングループ", "CONDITION_NAME": "条件名", "CRITERIA_FOR_EXECUTING_ACTIONS": "アクションの実行条件", diff --git a/src/lib/actionLayout.json b/src/lib/converter/helper/actionLayout.json similarity index 100% rename from src/lib/actionLayout.json rename to src/lib/converter/helper/actionLayout.json diff --git a/src/lib/actionParser.ts b/src/lib/converter/helper/actionParser.ts similarity index 94% rename from src/lib/actionParser.ts rename to src/lib/converter/helper/actionParser.ts index 1de5206..e7d3b1d 100644 --- a/src/lib/actionParser.ts +++ b/src/lib/converter/helper/actionParser.ts @@ -1,8 +1,8 @@ -import { ActionCall, InputParamValue } from '../types/flow'; -import { RecordCreate, RecordUpdate, RecordFilter, RecordLookup } from '../types/flowRecordAction'; -import { toArray } from './util/arrayUtils'; -import { ProcessMetadataValue } from '../types/processMetadataValue'; -import { ReadableCondition, ReadableActionItem, ReadableActionItemParameter } from '../types/parser'; +import { ActionCall, InputParamValue } from '../../../types/metadata/flow'; +import { RecordCreate, RecordUpdate, RecordFilter, RecordLookup } from '../../../types/metadata/flowRecordAction'; +import { toArray } from '../../util/arrayUtils'; +import { ProcessMetadataValue } from '../../../types/metadata/processMetadataValue'; +import { ReadableCondition, ReadableActionItem, ReadableActionItemParameter } from '../../../types/converter/process'; const layout = require('./actionLayout.json'); diff --git a/src/lib/converter/helper/constants.ts b/src/lib/converter/helper/constants.ts new file mode 100644 index 0000000..d462080 --- /dev/null +++ b/src/lib/converter/helper/constants.ts @@ -0,0 +1,3 @@ +export const SUPPORTED_PROCESS = ['Workflow', 'CustomEvent', 'InvocableProcess']; +export const SUPPORTED_FLOW = ['AutoLaunchedFlow']; +export const API_VERSION = '49.0'; diff --git a/src/lib/converter/helper/readableFlow.ts b/src/lib/converter/helper/readableFlow.ts new file mode 100644 index 0000000..60de8bb --- /dev/null +++ b/src/lib/converter/helper/readableFlow.ts @@ -0,0 +1,211 @@ +import ReadableMetadata from '../readableMetadata'; +import { + ReadableFlow, + ReadableAssignmentItem, + ReadableAssignment, + ReadableFlowBuilderItem, + ReadableLoop, + ReadableFlowDecisionRoute, + ReadableFlowDecision, + ReadableRecordLookup, +} from '../../../types/converter/flow'; +import { + Flow, + FlowBuilderItem, + implementsAssignment, + Assignment, + AssignmentItem, + Loop, + implementsLoop, + implementsDecision, + Decision, +} from '../../../types/metadata/flow'; +import { toArray } from '../../util/arrayUtils'; +import ReadableFlowStartElement from './readableFlowStart'; +import { RecordLookup, implementsRecordLookup } from '../../../types/metadata/flowRecordAction'; + +export default class ReadableFlowMetadata extends ReadableMetadata { + flowBuilderItems: ReadonlyArray; + + visitedItemNameSet: Set; + + constructor(flow: Flow, name: string) { + super(flow, name); + this.flowBuilderItems = [ + ...this.flow.actionCalls, + ...this.flow.assignments, + ...this.flow.loops, + ...this.flow.decisions, + ...this.flow.recordLookups, + ...this.flow.recordCreates, + ...this.flow.recordUpdates, + ...this.flow.recordDeletes, + ]; + this.visitedItemNameSet = new Set(); + } + + createReadableFlow(): ReadableFlow { + const start = new ReadableFlowStartElement(this.flow); + + return { + name: this.name, + label: this.getLabel(), + processType: this.getProcessType(), + description: this.getDescription(), + start: start.getReadableElement(), + elements: this.getReadableFlowElements([], this.getFirstElementName()), + }; + } + + private getFirstElementName() { + return this.flow.start.connector ? this.flow.start.connector.targetReference : undefined; + } + + // Array + private getReadableFlowElements( + currentElements: Array, + targetReference: string + ): Array { + if (!targetReference) { + return currentElements; + } + const nextElement = this.flowBuilderItems.find(e => e.name === targetReference); + if (nextElement) { + this.visitedItemNameSet.add(nextElement.name); + // Get the details of element and find next. Elements inside loop and decision results are traced in each convert method. + let nextReference; + if (implementsLoop(nextElement)) { + nextReference = nextElement.noMoreValuesConnector + ? nextElement.noMoreValuesConnector.targetReference + : undefined; + currentElements.push(this.convertToReadableLoop(nextElement)); + } else if (implementsDecision(nextElement)) { + currentElements.push(this.convertToReadableDecision(nextElement)); + } else { + nextReference = nextElement.connector ? nextElement.connector.targetReference : undefined; + currentElements.push(this.convertToReadableFlowElement(nextElement)); + } + this.getReadableFlowElements(currentElements, nextReference); + } + return currentElements; + } + + private convertToReadableFlowElement(flowBuilderItem: FlowBuilderItem): ReadableFlowBuilderItem { + if (implementsAssignment(flowBuilderItem)) { + return this.convertToReadableAssingment(flowBuilderItem); + } + if (implementsRecordLookup(flowBuilderItem)) { + return this.convertToReadableRecordLookup(flowBuilderItem); + } + return undefined; + } + + private convertToReadableDecision(decision: Decision): ReadableFlowDecision { + const defaultRoute: ReadableFlowDecisionRoute = { + name: decision.defaultConnectorLabel, + label: undefined, + elements: this.traceDecisionRouteElements([], decision.defaultConnector.targetReference), + }; + const rules = toArray(decision.rules); + const ruleRoutes: Array = []; + for (const rule of rules) { + if (rule.connector) { + ruleRoutes.push({ + name: rule.name, + label: rule.label, + elements: this.traceDecisionRouteElements([], rule.connector.targetReference), + }); + } + } + return { + type: 'decision', + name: decision.name, + label: decision.label, + routes: [defaultRoute, ...ruleRoutes], + }; + } + + private traceDecisionRouteElements( + routeElements: Array, + targetReference: string + ): Array { + if (!targetReference || this.visitedItemNameSet.has(targetReference)) { + return routeElements; + } + const nextElement = this.flowBuilderItems.find(e => e.name === targetReference); + if (nextElement) { + routeElements.push({ label: nextElement.label, name: nextElement.name }); + let nextReference; + if (implementsLoop(nextElement)) { + nextReference = nextElement.noMoreValuesConnector + ? nextElement.noMoreValuesConnector.targetReference + : undefined; + } else if (implementsDecision(nextElement)) { + nextReference = nextElement.defaultConnector ? nextElement.defaultConnector.targetReference : undefined; + } else { + nextReference = nextElement.connector ? nextElement.connector.targetReference : undefined; + } + this.traceDecisionRouteElements(routeElements, nextReference); + } + return routeElements; + } + + private convertToReadableAssingment(assignment: Assignment): ReadableAssignment { + const assignments: Array = toArray(assignment.assignmentItems).map( + item => ({ + reference: item.assignToReference, + operator: item.operator, + value: this.resolveValue(item.value), + }) + ); + return { + type: 'assignment', + name: assignment.name, + label: assignment.label, + description: assignment.description, + assignments, + }; + } + + private convertToReadableLoop(loop: Loop): ReadableLoop { + const elements = loop.nextValueConnector + ? this.getReadableFlowElements([], loop.nextValueConnector.targetReference) + : []; + return { + type: 'loop', + name: loop.name, + label: loop.label, + description: loop.description, + elements, + }; + } + + private convertToReadableRecordLookup(lookup: RecordLookup): ReadableRecordLookup { + return { + type: 'lookup', + name: lookup.name, + label: lookup.label, + object: lookup.object, + filterCondition: lookup.filters ? 'MEET_ALL_CONDITIONS' : 'NONE', + filters: this.getReadableFlowRecordLookupFilters(lookup.filters), + sortBy: { + order: lookup.sortOrder ? lookup.sortOrder.toUpperCase() : 'NONE', + }, + numberOfRecords: lookup.getFirstRecordOnly ? 'ONLY_FIRST' : 'ALL', + output: { + method: lookup.storeOutputAutomatically ? 'ALL_FIELDS' : undefined, + }, + }; + } + + getReadableFlowRecordLookupFilters = filters => { + if (!filters) { + return undefined; + } + return toArray(filters).map(f => ({ + field: f.field, + operator: f.operator, + value: this.resolveValue(f.value), + })); + }; +} diff --git a/src/lib/converter/helper/readableFlowStart.ts b/src/lib/converter/helper/readableFlowStart.ts new file mode 100644 index 0000000..6124f3c --- /dev/null +++ b/src/lib/converter/helper/readableFlowStart.ts @@ -0,0 +1,47 @@ +import { ReadableStart } from '../../../types/converter/flow'; + +export default class ReadableFlowStartElement { + flow; + + constructor(flow) { + this.flow = flow; + } + + getReadableElement(): ReadableStart { + const triggerType = this.getFlowTriggerType(); + return { + triggerType, // record, schedule, or auto-launch + object: this.flow.start.object, + recordTriggerType: this.getFlowRecordTriggerType(), // create, update, or both + schedule: triggerType === 'FLOW_TRIGGER_SCHEDULED' ? this.flow.start.schedule : undefined, + context: triggerType === 'FLOW_TRIGGER_RECORD' ? this.getRecordFlowContext() : undefined, // before or after + }; + } + + private getFlowTriggerType() { + if (this.flow.start.recordTriggerType) { + return 'FLOW_TRIGGER_RECORD'; + } + if (this.flow.start.schedule) { + return 'FLOW_TRIGGER_SCHEDULED'; + } + return 'FLOW_TRIGGER_USER_OR_APPS'; + } + + private getFlowRecordTriggerType() { + switch (this.flow.start.recordTriggerType) { + case 'Create': + return 'FROW_TRIGGER_RECORD_CREATE_ONLY'; + case 'Update': + return 'FROW_TRIGGER_RECORD_UPDATE_ONLY'; + case 'CreateAndUpdate': + return 'FROW_TRIGGER_RECORD_CREATE_OR_UPDATE'; + default: + return undefined; + } + } + + private getRecordFlowContext() { + return this.flow.start.triggerType === 'RecordBeforeSave' ? 'BEFORE_SAVE' : 'AFTER_SAVE'; + } +} diff --git a/src/lib/flowParser.ts b/src/lib/converter/helper/readableProcess.ts similarity index 62% rename from src/lib/flowParser.ts rename to src/lib/converter/helper/readableProcess.ts index ff0fc7e..fcda4da 100644 --- a/src/lib/flowParser.ts +++ b/src/lib/converter/helper/readableProcess.ts @@ -1,65 +1,30 @@ -import { Flow, Decision, ActionCall, implementsActionCall, InputParamValue, IteratableFlow } from '../types/flow'; +import ReadableMetadata from '../readableMetadata'; import { - RecordCreate, - RecordUpdate, - implementsRecordCreate, - implementsRecordUpdate, - RecordLookup, -} from '../types/flowRecordAction'; + ReadableActionGroup, + ReadableCondition, + ReadableActionItem, + ReadableScheduledActionSection, + ReadableWaitEventSummary, + ReadableProcessDecision, +} from '../../../types/converter/process'; +import { ActionCall, implementsActionCall, Decision } from '../../../types/metadata/flow'; import { + getRecordLookupFilter, convertToReadableActionCall, convertToReadableRecordCreate, convertToReadableRecordUpdate, - getRecordLookupFilter, } from './actionParser'; -import { toArray } from './util/arrayUtils'; -import { implementsProcessMetadataValue, ProcessMetadataValue } from '../types/processMetadataValue'; -import { unescapeHtml } from './util/stringUtils'; import { - ReadableProcess, - ReadableCondition, - ReadableActionGroup, - ReadableDecision, - ReadableActionItem, - ReadableScheduledActionSection, - ReadableWaitEventSummary, -} from '../types/parser'; - -export default class FlowParser { - private readonly flow: IteratableFlow; - - private readonly name: string; - - constructor(flow: Flow, name: string) { - this.flow = flow as IteratableFlow; - this.name = name; - - for (const arrayName of [ - 'processMetadataValues', - 'decisions', - 'actionCalls', - 'recordLookups', - 'formulas', - 'waits', - ]) { - this.flow[arrayName] = toArray(flow[arrayName]); - } - - const rawRecordUpdates = toArray(this.flow.recordUpdates); - this.flow.recordUpdates = - rawRecordUpdates.length !== 0 ? rawRecordUpdates.map(a => ({ ...a, actionType: 'recordUpdate' })) : []; - - const rawRecordCreates = toArray(this.flow.recordCreates); - this.flow.recordCreates = - rawRecordCreates.length !== 0 ? rawRecordCreates.map(a => ({ ...a, actionType: 'recordCreate' })) : []; - } - - isSupportedFlow() { - return ['Workflow', 'CustomEvent', 'InvocableProcess'].includes(this.flow.processType); - } + RecordUpdate, + RecordCreate, + implementsRecordCreate, + implementsRecordUpdate, +} from '../../../types/metadata/flowRecordAction'; +import { toArray } from '../../util/arrayUtils'; - createReadableProcess(): ReadableProcess { - const result: ReadableProcess = { +export default class ReadableProcessMetadata extends ReadableMetadata { + createReadableProcess() { + return { name: this.name, label: this.getLabel(), processType: this.getProcessType(), @@ -70,67 +35,25 @@ export default class FlowParser { eventMatchingConditions: this.getEventMatchingConditions(), actionGroups: this.getReadableActionGroups(), }; - return result; - } - - getLabel() { - return this.flow.label; - } - - private getProcessType() { - return this.flow.processType; - } - - /** - * Returns flow description - */ - private getDescription() { - return this.flow.description ? this.flow.description : ''; - } - - /** - * Returns sobject type of the flow - */ - private getObjectType() { - return this.flow.processMetadataValues.find(p => p.name === 'ObjectType').value.stringValue; - } - - /** - * Returns trigger type (e.g., only in create, both create and edit) for workflow type process - */ - private getTriggerType() { - const triggerTypePmv = this.flow.processMetadataValues.find(p => p.name === 'TriggerType'); - return triggerTypePmv ? triggerTypePmv.value.stringValue : undefined; - } - - /** - * Returns platform event type - */ - private getEventType() { - const eventTypePmv = this.flow.processMetadataValues.find(p => p.name === 'EventType'); - return eventTypePmv ? eventTypePmv.value.stringValue : undefined; } /** * Returns matching condition for platform event based process */ - getEventMatchingConditions(): Array { + private getEventMatchingConditions(): Array { if (this.getProcessType() === 'CustomEvent') { - const startElementName = this.getStartElement(); + const startElementName = this.getStartElementName(); const recordLookup = this.getRecordLookup(startElementName); return getRecordLookupFilter(this, recordLookup); } return undefined; } - private getStartElement() { - return this.flow.startElementReference; - } - /** * Returns readable action group (decision and actions) based on the metadata + * @returns {Array} */ - getReadableActionGroups(): Array { + private getReadableActionGroups(): Array { const actionGroups: Array = []; const decisions = this.getStandardDecisions(); for (const d of decisions) { @@ -156,8 +79,20 @@ export default class FlowParser { return actionGroups; } - convertToReadableDecision(decision: Decision): ReadableDecision { - const readableDecision: ReadableDecision = { + /* Decisions */ + private getStandardDecisions(): Array { + return this.flow.decisions + .filter(d => d.processMetadataValues !== undefined) + .sort((d1, d2) => { + return ( + Number(d1.processMetadataValues.value.numberValue) - + Number(d2.processMetadataValues.value.numberValue) + ); + }); + } + + private convertToReadableDecision(decision: Decision): ReadableProcessDecision { + const readableDecision: ReadableProcessDecision = { label: decision.rules.label, criteria: this.getActionExecutionCriteria(decision), conditionLogic: decision.rules.conditionLogic.toUpperCase(), @@ -169,7 +104,7 @@ export default class FlowParser { return readableDecision; } - getReadableDecisionConditions(decision: Decision): Array { + private getReadableDecisionConditions(decision: Decision): Array { const rawConditions = toArray(decision.rules.conditions); const result: Array = []; for (const c of rawConditions) { @@ -185,89 +120,10 @@ export default class FlowParser { return result; } - convertToReadableActionItems(actions: Array): Array { - return actions.map(a => this.convertToReadableActionItem(a)); - } - - convertToReadableActionItem = (action: ActionCall | RecordCreate | RecordUpdate): ReadableActionItem => { - if (implementsActionCall(action)) { - return convertToReadableActionCall(this, action); - } - if (implementsRecordCreate(action)) { - return convertToReadableRecordCreate(this, action); - } - if (implementsRecordUpdate(action)) { - return convertToReadableRecordUpdate(this, action); - } - return undefined; - }; - - getReadableScheduledActionSections(waitName: string): Array { - const wait = this.flow.waits.find(a => a.name === waitName); - if (!wait) { - return undefined; - } - const waitEvents = toArray(wait.waitEvents); - const sections: Array = []; - for (const waitEvent of waitEvents) { - const waitEventSummary = this.getReadableWaitEventSummary(waitEvent); - const waitEventActions = this.getWaitEventActions( - waitEvent, - Object.prototype.hasOwnProperty.call(waitEventSummary, 'field') - ); - const section: ReadableScheduledActionSection = { - summary: waitEventSummary, - actions: this.convertToReadableActionItems(waitEventActions), - }; - sections.push(section); - } - return sections; - } - - getReadableWaitEventSummary = (waitEvent): ReadableWaitEventSummary => { - const inputParams = toArray(waitEvent.inputParameters); - const rawTimeOffset = Number(inputParams.find(i => i.name === 'TimeOffset').value.numberValue); - const referencedFieldParam = inputParams.find(i => i.name === 'TimeFieldColumnEnumOrId'); - const summary: ReadableWaitEventSummary = { - offset: Math.abs(rawTimeOffset), - isAfter: rawTimeOffset > 0, - unit: inputParams.find(i => i.name === 'TimeOffsetUnit').value.stringValue, - }; - if (referencedFieldParam) { - summary.field = referencedFieldParam.value.stringValue; - } - return summary; - }; - - getWaitEventActions(waitEvent, comparedToField) { - let nextReference = waitEvent.connector.targetReference; - if (comparedToField) { - const decision = this.getDecision(nextReference); - if (!decision) { - return []; - } - nextReference = decision.rules.connector.targetReference; - } - return this.getActionSequence([], nextReference); - } - - private getDecision(name: string) { - return this.flow.decisions.find(d => d.name === name); - } - - private getRecordLookup(name: string): RecordLookup { - return this.flow.recordLookups.find(r => r.name === name); - } - - private getStandardDecisions(): Array { - return this.flow.decisions - .filter(d => d.processMetadataValues !== undefined) - .sort((d1, d2) => { - return ( - Number(d1.processMetadataValues.value.numberValue) - - Number(d2.processMetadataValues.value.numberValue) - ); - }); + private getDecisionFormulaExpression(decision: Decision): string { + const formulaName = decision.rules.conditions.leftValueReference; + const formulaExpression = this.getFormulaExpression(formulaName); + return formulaExpression ? unescape(formulaExpression) : undefined; } private getActionExecutionCriteria(decision: Decision) { @@ -290,14 +146,6 @@ export default class FlowParser { return 'CONDITIONS_ARE_MET'; } - /** - * Check if the given formula has always return true - * This is used in 'no criteria' decision. - */ - private hasAlwaysTrueFormula(name) { - return this.flow.formulas.some(f => f.name === name && f.dataType === 'Boolean' && f.expression === 'true'); - } - private hasIsChangedCondition(name) { return this.flow.decisions.some(d => d.rules && !Array.isArray(d.rules) && d.rules.name === name); } @@ -308,6 +156,21 @@ export default class FlowParser { return this.resolveValue(targetCondition.rightValue); } + private getConditionType = condition => { + if (condition.processMetadataValues) { + return condition.processMetadataValues.find(p => p.name === 'rightHandSideType').value.stringValue; + } + return undefined; + }; + + /** + * Check if the given formula has always return true. This method is used in 'no criteria' decision. + */ + private hasAlwaysTrueFormula(name) { + return this.flow.formulas.some(f => f.name === name && f.dataType === 'Boolean' && f.expression === 'true'); + } + + /* Actions */ private getActionSequence(actions: Array, nextActionName: string) { const nextAction = this.getAction(nextActionName); if (nextAction) { @@ -336,62 +199,72 @@ export default class FlowParser { ); } - private getConditionType = condition => { - if (condition.processMetadataValues) { - return condition.processMetadataValues.find(p => p.name === 'rightHandSideType').value.stringValue; + private convertToReadableActionItems( + actions: Array + ): Array { + return actions.map(a => this.convertToReadableActionItem(a)); + } + + private convertToReadableActionItem = (action: ActionCall | RecordCreate | RecordUpdate): ReadableActionItem => { + if (implementsActionCall(action)) { + return convertToReadableActionCall(this, action); + } + if (implementsRecordCreate(action)) { + return convertToReadableRecordCreate(this, action); + } + if (implementsRecordUpdate(action)) { + return convertToReadableRecordUpdate(this, action); } return undefined; }; - private getFormulaExpression(name) { - const formula = this.flow.formulas.find(f => f.name === name); - return formula ? formula.processMetadataValues.value.stringValue : undefined; - } - - private getDecisionFormulaExpression(decision: Decision): string { - const formulaName = decision.rules.conditions.leftValueReference; - const formulaExpression = this.getFormulaExpression(formulaName); - return formulaExpression ? unescape(formulaExpression) : undefined; - } - - private getObjectVariable(name) { - const variable = this.flow.variables.find(v => v.name === name); - return variable.objectType; - } - - resolveValue = (value: string | InputParamValue | ProcessMetadataValue) => { - if (!value) { - return '$GlobalConstant.null'; - } - // Chatter Message - if (implementsProcessMetadataValue(value)) { - if (value.name === 'textJson') { - return JSON.parse(unescapeHtml(value.value.stringValue)).message; - } - const key = Object.keys(value.value)[0]; - return value.value[key]; + /* Scheduled Actions */ + private getReadableScheduledActionSections(waitName: string): Array { + const wait = this.flow.waits.find(a => a.name === waitName); + if (!wait) { + return undefined; } - // String - if (typeof value === 'string') { - return this.replaceVariableNameToObjectName(value); + const waitEvents = toArray(wait.waitEvents); + const sections: Array = []; + for (const waitEvent of waitEvents) { + const waitEventSummary = this.getReadableWaitEventSummary(waitEvent); + const waitEventActions = this.getWaitEventActions( + waitEvent, + Object.prototype.hasOwnProperty.call(waitEventSummary, 'field') + ); + const section: ReadableScheduledActionSection = { + summary: waitEventSummary, + actions: this.convertToReadableActionItems(waitEventActions), + }; + sections.push(section); } - // Object - const key = Object.keys(value)[0]; // stringValue or elementReference - if (key === 'elementReference') { - if (!value[key].includes('.')) { - return this.getFormulaExpression(value[key]); - } - return this.replaceVariableNameToObjectName(value[key]); + return sections; + } + + private getReadableWaitEventSummary = (waitEvent): ReadableWaitEventSummary => { + const inputParams = toArray(waitEvent.inputParameters); + const rawTimeOffset = Number(inputParams.find(i => i.name === 'TimeOffset').value.numberValue); + const referencedFieldParam = inputParams.find(i => i.name === 'TimeFieldColumnEnumOrId'); + const summary: ReadableWaitEventSummary = { + offset: Math.abs(rawTimeOffset), + isAfter: rawTimeOffset > 0, + unit: inputParams.find(i => i.name === 'TimeOffsetUnit').value.stringValue, + }; + if (referencedFieldParam) { + summary.field = referencedFieldParam.value.stringValue; } - return value[key]; + return summary; }; - private replaceVariableNameToObjectName(string) { - if (!string.includes('.')) { - return string; + private getWaitEventActions(waitEvent, comparedToField) { + let nextReference = waitEvent.connector.targetReference; + if (comparedToField) { + const decision = this.getDecision(nextReference); + if (!decision) { + return []; + } + nextReference = decision.rules.connector.targetReference; } - const variableName = string.split('.')[0]; - const objectName = this.getObjectVariable(variableName); - return string.replace(variableName, `[${objectName}]`); + return this.getActionSequence([], nextReference); } } diff --git a/src/lib/converter/iteratableMetadata.ts b/src/lib/converter/iteratableMetadata.ts new file mode 100644 index 0000000..9e135da --- /dev/null +++ b/src/lib/converter/iteratableMetadata.ts @@ -0,0 +1,34 @@ +import { toArray } from '../util/arrayUtils'; +import { Flow } from '../../types/metadata/flow'; +import { IteratableFlow } from '../../types/converter/main'; + +export default function convertToIteratableMetadata(flow: Flow): IteratableFlow { + return { + processType: flow.processType, + label: flow.label, + description: flow.description, + startElementReference: flow.startElementReference, + variables: toArray(flow.variables), + formulas: toArray(flow.formulas), + waits: toArray(flow.waits), + start: flow.start, + processMetadataValues: toArray(flow.processMetadataValues), + assignments: toArray(flow.assignments), + loops: toArray(flow.loops), + decisions: toArray(flow.decisions), + actionCalls: toArray(flow.actionCalls), + recordLookups: toArray(flow.recordLookups), + recordCreates: + toArray(flow.recordCreates).length > 0 + ? toArray(flow.recordCreates).map(action => ({ ...action, actionType: 'recordCreate' })) + : [], + recordUpdates: + toArray(flow.recordUpdates).length > 0 + ? toArray(flow.recordUpdates).map(action => ({ ...action, actionType: 'recordUpdate' })) + : [], + recordDeletes: + toArray(flow.recordDeletes).length > 0 + ? toArray(flow.recordDeletes).map(action => ({ ...action, actionType: 'recordDelete' })) + : [], + }; +} diff --git a/src/lib/converter/metadataConverter.ts b/src/lib/converter/metadataConverter.ts new file mode 100644 index 0000000..765c4f8 --- /dev/null +++ b/src/lib/converter/metadataConverter.ts @@ -0,0 +1,47 @@ +import { Flow } from '../../types/metadata/flow'; +import { ReadableProcess } from '../../types/converter/process'; +import { ReadableFlow } from '../../types/converter/flow'; +import ReadableFlowMetadata from './helper/readableFlow'; +import ReadableProcessMetadata from './helper/readableProcess'; + +interface MetadataConverter { + accept(builder: DocumentBuilder); +} + +export class ReadableFlowMetadataConverter implements MetadataConverter { + readableMetadata: ReadableFlow; + + constructor(flow: Flow, name: string) { + const metadataConverter = new ReadableFlowMetadata(flow, name); + this.readableMetadata = metadataConverter.createReadableFlow(); + } + + accept(builder: DocumentBuilder) { + return builder.buildFlowDocument(this); + } +} + +export class ReadableProcessMetadataConverter implements MetadataConverter { + readableMetadata: ReadableProcess; + + constructor(flow: Flow, name: string) { + const metadataConverter = new ReadableProcessMetadata(flow, name); + this.readableMetadata = metadataConverter.createReadableProcess(); + } + + accept(builder: DocumentBuilder) { + return builder.buildProcessDocument(this); + } +} + +export abstract class DocumentBuilder { + protected locale: string; + + constructor(locale: string) { + this.locale = locale; + } + + abstract buildFlowDocument(converter: ReadableFlowMetadataConverter): any; + + abstract buildProcessDocument(converter: ReadableProcessMetadataConverter): any; +} diff --git a/src/lib/converter/readableMetadata.ts b/src/lib/converter/readableMetadata.ts new file mode 100644 index 0000000..7938a70 --- /dev/null +++ b/src/lib/converter/readableMetadata.ts @@ -0,0 +1,131 @@ +import { InputParamValue, Flow } from '../../types/metadata/flow'; +import { IteratableFlow } from '../../types/converter/main'; +import { RecordLookup } from '../../types/metadata/flowRecordAction'; +import { implementsProcessMetadataValue, ProcessMetadataValue } from '../../types/metadata/processMetadataValue'; +import { unescapeHtml } from '../util/stringUtils'; +import convertToIteratableMetadata from './iteratableMetadata'; +import { isProcess } from '../util/flowUtils'; + +export default class ReadableMetadata { + protected readonly flow: IteratableFlow; + + protected readonly name: string; + + protected readonly isProcess: boolean; + + constructor(flow: Flow, name: string) { + this.flow = convertToIteratableMetadata(flow); + this.name = name; + this.isProcess = isProcess(flow); + } + + /** + * @return {string} Returns display label of the flow + */ + public getLabel(): string { + return this.flow.label; + } + + /** + * @return {strign} Returns type of the flow / process + */ + protected getProcessType() { + return this.flow.processType; + } + + /** + * @return {string} Returns flow description + */ + protected getDescription() { + return this.flow.description ? this.flow.description : ''; + } + + /** + * @return {string} Returns sObject type of the flow + */ + protected getObjectType() { + return this.flow.processMetadataValues.find(p => p.name === 'ObjectType').value.stringValue; + } + + /** + * @return {string} Returns trigger type + */ + protected getTriggerType() { + const triggerTypePmv = this.flow.processMetadataValues.find(p => p.name === 'TriggerType'); + return triggerTypePmv ? triggerTypePmv.value.stringValue : undefined; + } + + /** + * @retuns {string} Returns platform event type + */ + protected getEventType() { + const eventTypePmv = this.flow.processMetadataValues.find(p => p.name === 'EventType'); + return eventTypePmv ? eventTypePmv.value.stringValue : undefined; + } + + /** + * @returns {string} Returns name of start element + */ + protected getStartElementName() { + return this.flow.startElementReference; + } + + protected getDecision(name: string) { + return this.flow.decisions.find(d => d.name === name); + } + + protected getRecordLookup(name: string): RecordLookup { + return this.flow.recordLookups.find(r => r.name === name); + } + + /** + * @returns {string} Returns formula expression + */ + protected getFormulaExpression(name) { + const formula = this.flow.formulas.find(f => f.name === name); + return formula ? formula.processMetadataValues.value.stringValue : undefined; + } + + private getObjectVariable(name) { + const variable = this.flow.variables.find(v => v.name === name); + return variable.objectType; + } + + resolveValue = (value: string | InputParamValue | ProcessMetadataValue) => { + if (!value) { + return '$GlobalConstant.null'; + } + // Chatter Message + if (implementsProcessMetadataValue(value)) { + if (value.name === 'textJson') { + return JSON.parse(unescapeHtml(value.value.stringValue)).message; + } + const key = Object.keys(value.value)[0]; + return value.value[key]; + } + // String + if (typeof value === 'string') { + return this.replaceVariableNameToObjectName(value); + } + // Object + const key = Object.keys(value)[0]; // stringValue or elementReference + if (key === 'elementReference') { + if (!value[key].includes('.')) { + // reference to formula + return this.getFormulaExpression(value[key]); + } + return this.replaceVariableNameToObjectName(value[key]); + } + return value[key]; + }; + + // for process builder + private replaceVariableNameToObjectName(string) { + if (!string.includes('.') || !this.isProcess) { + return string; + } + const variableName = string.split('.')[0]; + const objectName = this.getObjectVariable(variableName); + return string.replace(variableName, `[${objectName}]`); + } +} diff --git a/src/lib/docx/docxBuilder.ts b/src/lib/docx/docxBuilder.ts index 3ecc3c8..261146d 100644 --- a/src/lib/docx/docxBuilder.ts +++ b/src/lib/docx/docxBuilder.ts @@ -1,22 +1,34 @@ import * as docx from 'docx'; import * as path from 'path'; import * as fs from 'fs'; -import { ReadableProcess } from '../../types/parser'; import DocxProcessFormatter from './process/docxProcessFormatter'; +import { + DocumentBuilder, + ReadableProcessMetadataConverter, + ReadableFlowMetadataConverter, +} from '../converter/metadataConverter'; -export default function buildDocxContent(flow: ReadableProcess, locale: string): docx.File { - const jaStyles = fs.readFileSync(path.resolve(__dirname, '../../assets/fonts/styles.ja.xml'), 'utf-8'); - const enStyles = fs.readFileSync(path.resolve(__dirname, '../../assets/fonts/styles.en.xml'), 'utf-8'); - const content = []; - const dpf = new DocxProcessFormatter(flow, locale); - content.push(...dpf.prepareHeader()); - content.push(...dpf.prepareStartCondition()); - content.push(...dpf.prepareActionGroups()); - const doc = new docx.Document({ - externalStyles: locale === 'ja' ? jaStyles : enStyles, - }); - doc.addSection({ - children: content, - }); - return doc; +const jaStyles = fs.readFileSync(path.resolve(__dirname, '../../assets/fonts/styles.ja.xml'), 'utf-8'); +const enStyles = fs.readFileSync(path.resolve(__dirname, '../../assets/fonts/styles.en.xml'), 'utf-8'); + +export default class DocxBuilder extends DocumentBuilder { + buildFlowDocument(converter: ReadableFlowMetadataConverter) { + console.log(this.locale); + console.log(converter.readableMetadata); + } + + buildProcessDocument(converter: ReadableProcessMetadataConverter) { + const content = []; + const dpf = new DocxProcessFormatter(converter.readableMetadata, this.locale); + content.push(...dpf.prepareHeader()); + content.push(...dpf.prepareStartCondition()); + content.push(...dpf.prepareActionGroups()); + const doc = new docx.Document({ + externalStyles: this.locale === 'ja' ? jaStyles : enStyles, + }); + doc.addSection({ + children: content, + }); + return doc; + } } diff --git a/src/lib/docx/process/docxProcessFormatter.ts b/src/lib/docx/process/docxProcessFormatter.ts index 055d8ce..e5a8338 100644 --- a/src/lib/docx/process/docxProcessFormatter.ts +++ b/src/lib/docx/process/docxProcessFormatter.ts @@ -4,7 +4,7 @@ import { ReadableActionItem, ReadableActionItemParameter, ReadableWaitEventSummary, -} from '../../../types/parser'; +} from '../../../types/converter/process'; import i18n from '../../../config/i18n'; import StartConditionFormatter from './startConditionFormatter'; import { diff --git a/src/lib/docx/process/docxTableUtils.ts b/src/lib/docx/process/docxTableUtils.ts index b77ce0a..50b9bd6 100644 --- a/src/lib/docx/process/docxTableUtils.ts +++ b/src/lib/docx/process/docxTableUtils.ts @@ -1,5 +1,5 @@ import { Table, TableRow, TableCell, Paragraph, TextRun, BorderStyle } from 'docx'; -import { ReadableCondition, ReadableActionItemParameter } from '../../../types/parser'; +import { ReadableCondition, ReadableActionItemParameter } from '../../../types/converter/process'; const CELL_DEFAULT_MARGIN = { top: 20, diff --git a/src/lib/docx/process/startConditionFormatter.ts b/src/lib/docx/process/startConditionFormatter.ts index e28ccd5..c8ce401 100644 --- a/src/lib/docx/process/startConditionFormatter.ts +++ b/src/lib/docx/process/startConditionFormatter.ts @@ -1,5 +1,5 @@ import { Paragraph } from 'docx'; -import { ReadableProcess } from '../../../types/parser'; +import { ReadableProcess } from '../../../types/converter/process'; import { createHorizontalHeaderTable, createProcessConditionTable } from './docxTableUtils'; export default class StartConditionFormatter { diff --git a/src/lib/json/jsonBuilder.ts b/src/lib/json/jsonBuilder.ts index 4e52dc9..a452eaf 100644 --- a/src/lib/json/jsonBuilder.ts +++ b/src/lib/json/jsonBuilder.ts @@ -1,34 +1,47 @@ /* eslint-disable no-param-reassign */ import i18n from '../../config/i18n'; -import { ReadableProcess } from '../../types/parser'; import { toUpperSnakeCase } from '../util/stringUtils'; +import { + DocumentBuilder, + ReadableFlowMetadataConverter, + ReadableProcessMetadataConverter, +} from '../converter/metadataConverter'; -export default function buildLocalizedJson(flow: ReadableProcess, locale: string) { - const _i18n = i18n(locale); +export default class JsonBuilder extends DocumentBuilder { + i18n = i18n(this.locale); - if (flow.processType === 'Workflow') { - flow.triggerType = - flow.triggerType === 'onAllChanges' - ? _i18n.__('WHEN_A_RECORD_IS_CREATED_OR_EDITED') - : _i18n.__('ONLY_WHEN_A_RECORD_IS_CREATED'); + buildFlowDocument(converter: ReadableFlowMetadataConverter) { + // TODO: translate labels + console.log(`building json... ${this.i18n}`); + return converter.readableMetadata; } - if (flow.actionGroups) { - for (const actionGroup of flow.actionGroups) { - actionGroup.decision.criteria = _i18n.__(actionGroup.decision.criteria); - if (actionGroup.actions) { - for (const action of actionGroup.actions) { - if (action.details) { - for (const detail of action.details) { - detail.name = _i18n.__( - `ACTION_DETAIL_${toUpperSnakeCase(action.type)}_${toUpperSnakeCase(detail.name)}` - ); + buildProcessDocument(converter: ReadableProcessMetadataConverter) { + const flow = converter.readableMetadata; + if (flow.processType === 'Workflow') { + flow.triggerType = + flow.triggerType === 'onAllChanges' + ? this.i18n.__('WHEN_A_RECORD_IS_CREATED_OR_EDITED') + : this.i18n.__('ONLY_WHEN_A_RECORD_IS_CREATED'); + } + + if (flow.actionGroups) { + for (const actionGroup of flow.actionGroups) { + actionGroup.decision.criteria = this.i18n.__(actionGroup.decision.criteria); + if (actionGroup.actions) { + for (const action of actionGroup.actions) { + if (action.details) { + for (const detail of action.details) { + detail.name = this.i18n.__( + `ACTION_DETAIL_${toUpperSnakeCase(action.type)}_${toUpperSnakeCase(detail.name)}` + ); + } } + action.type = this.i18n.__(`ACTION_TYPE_${toUpperSnakeCase(action.type)}`); } - action.type = _i18n.__(`ACTION_TYPE_${toUpperSnakeCase(action.type)}`); } } } + return flow; } - return flow; } diff --git a/src/lib/pdf/flow/pdfFlowFormatter.ts b/src/lib/pdf/flow/pdfFlowFormatter.ts new file mode 100644 index 0000000..70ee2ee --- /dev/null +++ b/src/lib/pdf/flow/pdfFlowFormatter.ts @@ -0,0 +1,69 @@ +import i18n from '../../../config/i18n'; +import { th } from '../../../style/text'; +import { ReadableFlow } from '../../../types/converter/flow'; + +export default class PdfProcessFormatter { + flow: ReadableFlow; + + i18n; + + constructor(flow: ReadableFlow, locale) { + this.flow = flow; + this.i18n = i18n(locale); + } + + buildHeader() { + return [ + { text: this.flow.label, style: 'h1' }, + { text: this.flow.name }, + { text: this.flow.description, margin: [0, 5] }, + ]; + } + + buildStart = () => { + const content: Array = [ + { + text: `${this.i18n.__('HEADER_FLOW_TRIGGER')}: ${this.i18n.__(this.flow.start.triggerType)}`, + margin: [0, 10, 0, 5], + }, + ]; + + if (this.flow.start.triggerType === 'FLOW_TRIGGER_RECORD') { + content.push({ + layout: 'lightHorizontalLines', + table: { + widths: [200, 'auto'], + body: [ + [th(this.i18n.__('OBJECT')), this.flow.start.object], + [ + th(this.i18n.__('HEADER_FLOW_TRIGGER_RECORD')), + this.i18n.__(this.flow.start.recordTriggerType), + ], + ], + }, + }); + } + + if (this.flow.start.triggerType === 'FLOW_TRIGGER_SCHEDULED') { + content.push({ + layout: 'lightHorizontalLines', + table: { + widths: [200, 'auto'], + body: [ + [ + th(this.i18n.__('FLOW_TRIGGER_SCHEDULED_DATETIME')), + `${this.flow.start.schedule.startDate}${this.flow.start.schedule.startTime} `, + ], + [th(this.i18n.__('FLOW_TRIGGER_SCHEDULED_FREQUENCY')), this.flow.start.schedule.frequency], + ], + }, + }); + } + + return content; + }; + + buildElements = () => { + return []; + }; +} diff --git a/src/lib/pdf/pdfBuilder.ts b/src/lib/pdf/pdfBuilder.ts index dfe72f3..1c315a0 100644 --- a/src/lib/pdf/pdfBuilder.ts +++ b/src/lib/pdf/pdfBuilder.ts @@ -1,20 +1,41 @@ import PdfProcessFormatter from './process/pdfProcessFormatter'; -import { ReadableProcess } from '../../types/parser'; +import PdfFlowFormatter from './flow/pdfFlowFormatter'; +import { + DocumentBuilder, + ReadableFlowMetadataConverter, + ReadableProcessMetadataConverter, +} from '../converter/metadataConverter'; const styles = require('../../style/style.json'); -export default function buildPdfContent(flow: ReadableProcess, locale: string) { - const content = []; - const ppf = new PdfProcessFormatter(flow, locale); - content.push(...ppf.buildHeader()); +export default class PdfBuilder extends DocumentBuilder { + buildFlowDocument(converter: ReadableFlowMetadataConverter) { + const content = []; + const formatter = new PdfFlowFormatter(converter.readableMetadata, this.locale); + content.push(...formatter.buildHeader()); + content.push(...formatter.buildStart()); + content.push(...formatter.buildElements()); + return { + content, + styles, + defaultStyle: { + font: 'NotoSans', + }, + }; + } - content.push(...ppf.buildStartCondition()); - content.push(...ppf.buildActionGroups()); - return { - content, - styles, - defaultStyle: { - font: 'NotoSans', - }, - }; + buildProcessDocument(converter: ReadableProcessMetadataConverter) { + const content = []; + const formatter = new PdfProcessFormatter(converter.readableMetadata, this.locale); + content.push(...formatter.buildHeader()); + content.push(...formatter.buildStartCondition()); + content.push(...formatter.buildActionGroups()); + return { + content, + styles, + defaultStyle: { + font: 'NotoSans', + }, + }; + } } diff --git a/src/lib/pdf/process/pdfProcessFormatter.ts b/src/lib/pdf/process/pdfProcessFormatter.ts index 99bf597..5c9f8e8 100644 --- a/src/lib/pdf/process/pdfProcessFormatter.ts +++ b/src/lib/pdf/process/pdfProcessFormatter.ts @@ -2,11 +2,11 @@ import { th, h2, h3 } from '../../../style/text'; import i18n from '../../../config/i18n'; import { ReadableProcess, - ReadableDecision, + ReadableProcessDecision, ReadableCondition, ReadableActionItem, ReadableWaitEventSummary, -} from '../../../types/parser'; +} from '../../../types/converter/process'; import StartConditionFormatter from './startConditionFormatter'; import { toUpperSnakeCase } from '../../util/stringUtils'; @@ -76,7 +76,7 @@ export default class PdfProcessFormatter { return content; }; - private buildDecision(decision: ReadableDecision) { + private buildDecision(decision: ReadableProcessDecision) { const table = { unbreakable: true, table: { diff --git a/src/lib/pdf/process/startConditionFormatter.ts b/src/lib/pdf/process/startConditionFormatter.ts index ba2216c..586f138 100644 --- a/src/lib/pdf/process/startConditionFormatter.ts +++ b/src/lib/pdf/process/startConditionFormatter.ts @@ -1,5 +1,5 @@ import { th } from '../../../style/text'; -import { ReadableProcess } from '../../../types/parser'; +import { ReadableProcess } from '../../../types/converter/process'; export default class StartConditionFormatter { flow: ReadableProcess; diff --git a/src/lib/util/flowUtils.ts b/src/lib/util/flowUtils.ts new file mode 100644 index 0000000..816e56a --- /dev/null +++ b/src/lib/util/flowUtils.ts @@ -0,0 +1,15 @@ +import { Flow } from '../../types/metadata/flow'; +import { SUPPORTED_PROCESS, SUPPORTED_FLOW } from '../converter/helper/constants'; + +export function isSupported(flow: Flow) { + return ( + isProcess(flow) || + (SUPPORTED_FLOW.includes(flow.processType) && + flow.start.recordTriggerType !== undefined && + flow.start.triggerType === 'RecordBeforeSave') + ); +} + +export function isProcess(flow: Flow) { + return SUPPORTED_PROCESS.includes(flow.processType); +} diff --git a/src/types/converter/flow.ts b/src/types/converter/flow.ts new file mode 100644 index 0000000..3700d3d --- /dev/null +++ b/src/types/converter/flow.ts @@ -0,0 +1,93 @@ +import { SUPPORTED_FLOW } from '../../lib/converter/helper/constants'; +// WIP: import { RecordUpdate, RecordCreate, RecordLookup } from '../metadata/flowRecordAction'; + +/** + * Container for flow + */ +export interface ReadableFlow { + processType: string; + name: string; + label: string; + description?: string; + start: ReadableStart; + elements?: Array; +} + +export interface ReadableStart { + triggerType: string; + recordTriggerType?: string; // update, create, both + object?: string; + schedule?: { + startDate: string; + startTime: string; + frequency: string; + }; + context?: 'BEFORE_SAVE' | 'AFTER_SAVE'; +} + +export interface ReadableFlowElement { + type: string; + name: string; + label: string; + description?: string; +} + +export interface ReadableAssignment extends ReadableFlowElement { + assignments: Array; +} + +export interface ReadableAssignmentItem { + reference: string; + operator: string; + value: string; +} + +export interface ReadableLoop extends ReadableFlowElement { + elements: Array; +} + +export interface ReadableFlowDecision extends ReadableFlowElement { + routes: Array; +} + +export interface ReadableFlowDecisionRoute { + name: string; + label: string; + rule?: Array; + elements: Array; +} + +export interface ReadableCondition { + field: string; + operator: string; + type: string; + value: string; +} + +export interface ReadableRecordLookup extends ReadableFlowElement { + object: string; + filterCondition: 'NONE' | 'MEET_ALL_CONDITIONS'; + filters?: Array; + sortBy: { + order: string; + field?: string; + }; + numberOfRecords: 'ONLY_FIRST' | 'ALL'; + output: { + method: 'ALL_FIELDS' | 'CHOOSE_FIELDS' | 'CHOOSE_AND_ASSIGN_FIELDS'; + fields?: Array; + assignments?: Array; + }; +} + +export interface ReadableFlowRecordLookupFilter { + field: string; + operator: string; + value: string; +} + +export function implementsReadableFlow(arg: any): arg is ReadableFlow { + return SUPPORTED_FLOW.includes(arg.processType); +} + +export type ReadableFlowBuilderItem = ReadableAssignment | ReadableFlowDecision | ReadableLoop | ReadableRecordLookup; // | ReadableRecordCreate | ReadableRecordUpdate | ReadableRecordDelete ; diff --git a/src/types/converter/main.ts b/src/types/converter/main.ts new file mode 100644 index 0000000..af3c37c --- /dev/null +++ b/src/types/converter/main.ts @@ -0,0 +1,37 @@ +import { ProcessMetadataValue } from '../metadata/processMetadataValue'; +import { Variable, Decision, ActionCall } from '../metadata/flow'; +import { RecordUpdate, RecordCreate, RecordLookup, RecordDelete } from '../metadata/flowRecordAction'; + +/** + * Flow which has array parameters to find elements using prototype.array methods. + * (explicit array option is too redundant for a complex flow) + */ +export interface IteratableFlow { + processType: string; + label: string; + description: string; + startElementReference?: string; + variables: Array; + processMetadataValues: Array; + formulas: any; + decisions: Array; + assignments?: any; + actionCalls?: Array; + recordUpdates?: Array; + recordCreates?: Array; + recordLookups?: Array; + recordDeletes?: Array; + loops?: any; + waits: any; + start?: any; +} + +/** + * Base flow element + */ +export interface ReadableFlowElement { + type: string; + name: string; + label: string; + description?: string; +} diff --git a/src/types/parser.ts b/src/types/converter/process.ts similarity index 82% rename from src/types/parser.ts rename to src/types/converter/process.ts index 95d0e08..8d25c55 100644 --- a/src/types/parser.ts +++ b/src/types/converter/process.ts @@ -1,3 +1,5 @@ +import { SUPPORTED_PROCESS } from '../../lib/converter/helper/constants'; + export interface ReadableProcess { processType: string; name: string; @@ -10,14 +12,18 @@ export interface ReadableProcess { actionGroups: Array; } +export function implementsReadableProcess(arg: any): arg is ReadableProcess { + return SUPPORTED_PROCESS.includes(arg.processType); +} + export interface ReadableActionGroup { - decision: ReadableDecision; + decision: ReadableProcessDecision; actions?: Array; scheduledActionSections?: Array; evaluatesNext?: boolean; } -export interface ReadableDecision { +export interface ReadableProcessDecision { label: string; criteria: string; conditionLogic: string; diff --git a/src/types/flow.ts b/src/types/flow.ts deleted file mode 100644 index 4ac4789..0000000 --- a/src/types/flow.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { ProcessMetadataValue } from './processMetadataValue'; -import { RecordCreate, RecordUpdate, RecordLookup } from './flowRecordAction'; - -export interface Flow { - processType: string; - label: string; - description: string; - startElementReference: string; - variables: Variable | Array; - processMetadataValues: Array; - formulas: any; - decisions: Decision | Array; - actionCalls?: ActionCall | Array; - recordUpdates?: RecordUpdate | Array; - recordCreates?: RecordCreate | Array; - recordLookups?: RecordLookup | Array; - waits: any; -} - -export interface IteratableFlow { - processType: string; - label: string; - description: string; - startElementReference: string; - variables: Array; - processMetadataValues: Array; - formulas: any; - decisions: Array; - actionCalls?: Array; - recordUpdates?: Array; - recordCreates?: Array; - recordLookups?: Array; - waits: any; -} - -interface Variable { - name: string; - objectType: string; -} - -export interface ActionCall { - actionType: string; - name: string; - label: string; - connector: any; - processMetadataValues: any; - inputParameters: any; -} - -export function implementsActionCall(arg: any): arg is ActionCall { - return !(arg.actionType === 'recordUpdate' || arg.actionType === 'recordCreate'); -} - -export interface Decision { - name: string; - label: string; - processMetadataValues?: ProcessMetadataValue; - rules: any; - defaultConnector: any; -} - -export interface InputParamValue { - stringValue?: string; - elementReference?: string; -} diff --git a/src/types/metadata/flow.ts b/src/types/metadata/flow.ts new file mode 100644 index 0000000..e9da89f --- /dev/null +++ b/src/types/metadata/flow.ts @@ -0,0 +1,114 @@ +import { ProcessMetadataValue, ElementReferenceOrValue } from './processMetadataValue'; +import { Node, RecordCreate, RecordUpdate, RecordLookup, RecordDelete } from './flowRecordAction'; + +export interface Flow { + processType: string; + label: string; + description: string; + startElementReference?: string; + variables: Variable | Array; + processMetadataValues: Array; + formulas: any; + decisions: Decision | Array; + assignments: any; + actionCalls?: ActionCall | Array; + recordUpdates?: RecordUpdate | Array; + recordCreates?: RecordCreate | Array; + recordLookups?: RecordLookup | Array; + recordDeletes?: any; + loops?: any; + waits: any; + start?: any; +} + +export interface Variable { + name: string; + objectType: string; +} + +export interface ActionCall { + actionType: string; + name: string; + label: string; + connector: { + targetReference: string; + }; + processMetadataValues: any; + inputParameters: any; +} + +export function implementsActionCall(arg: any): arg is ActionCall { + return !(arg.actionType === 'recordUpdate' || arg.actionType === 'recordCreate'); +} + +export interface Decision extends Node { + processMetadataValues?: ProcessMetadataValue; + rules: any; + defaultConnector: any; + defaultConnectorLabel: string; +} + +export function implementsDecision(arg: any): arg is Decision { + return arg !== undefined && arg.defaultConnector !== undefined; +} + +export interface InputParamValue { + stringValue?: string; + elementReference?: string; +} + +export interface Assignment extends Node { + assignmentItems: AssignmentItem | Array; + connector: { + targetReference: string; + }; +} + +export function implementsAssignment(arg: any): arg is Assignment { + return arg !== undefined && arg.assignmentItems !== undefined; +} +export interface AssignmentItem { + assignToReference: string; + operator: + | 'Add' + | 'AddAdStart' + | 'AddItem' + | 'Assign' + | 'AssignCount' + | 'RemoveAfterFirst' + | 'RemoveAll' + | 'RemoveBeforeFirst' + | 'RemoveFirst' + | 'RemovePosition' + | 'RemoveUncommon' + | 'Subtract'; + value: ElementReferenceOrValue; +} + +export interface Loop extends Node { + nextValueConnector: { + targetReference: string; + }; + noMoreValuesConnector: { + targetReference: string; + }; + collectionReference: string; + assignNextValueToReference: string; + iterationOrder: 'Asc' | 'Dsc'; +} + +export function implementsLoop(arg: any): arg is Loop { + return arg !== undefined && arg.collectionReference !== undefined; +} + +export type ProcessElement = Decision | ActionCall; + +export type FlowBuilderItem = + | Decision + | Assignment + | Loop + | ActionCall + | RecordLookup + | RecordCreate + | RecordUpdate + | RecordDelete; diff --git a/src/types/flowRecordAction.ts b/src/types/metadata/flowRecordAction.ts similarity index 64% rename from src/types/flowRecordAction.ts rename to src/types/metadata/flowRecordAction.ts index 697c4cb..9494aca 100644 --- a/src/types/flowRecordAction.ts +++ b/src/types/metadata/flowRecordAction.ts @@ -1,6 +1,6 @@ import { ProcessMetadataValue, ElementReferenceOrValue } from './processMetadataValue'; -interface Node { +export interface Node { name: string; label: string; locationX: string; @@ -11,28 +11,48 @@ export interface RecordCreate extends Node { processMetadataValues?: any; actionType?: string; assignRecordIdToReference?: string; - connector: any; faultConnector?: any; inputAssignments: any; inputReference?: string; object: string; storeOutputAutomatically?: boolean; + connector: { + targetReference: string; + }; } export interface RecordUpdate extends Node { processMetadataValues?: any; actionType?: string; - connector: any; faultConnector?: any; filters: RecordFilter | RecordFilter[]; inputAssignments: any; object: string; inputReference?: string; + connector: { + targetReference: string; + }; } export interface RecordLookup extends Node { processMetadataValues?: any; + object: string; + getFirstRecordOnly?: boolean; + storeOutputAutomatically?: boolean; + sortOrder?: 'Asc' | 'Dsc'; filters?: RecordFilter | RecordFilter[]; + connector: { + targetReference: string; + }; +} + +export interface RecordDelete extends Node { + filters?: RecordFilter | RecordFilter[]; + object: string; + inputReference?: string; + connector: { + targetReference: string; + }; } export interface RecordFilter { @@ -47,3 +67,7 @@ export function implementsRecordCreate(arg: any): arg is RecordCreate { export function implementsRecordUpdate(arg: any): arg is RecordUpdate { return arg.actionType === 'recordUpdate'; } + +export function implementsRecordLookup(arg: any): arg is RecordLookup { + return arg.storeOutputAutomatically || arg.outputReference || arg.assignNullValuesIfNoRecordFound; +} diff --git a/src/types/processMetadataValue.ts b/src/types/metadata/processMetadataValue.ts similarity index 93% rename from src/types/processMetadataValue.ts rename to src/types/metadata/processMetadataValue.ts index 653ae7b..f0ea95f 100644 --- a/src/types/processMetadataValue.ts +++ b/src/types/metadata/processMetadataValue.ts @@ -7,6 +7,7 @@ export type ElementReferenceOrValue = RequireOne<{ stringValue?: string; numberValue?: string; booleanValue?: string; + dateValue?: string; elementReference?: string; }>; @@ -19,6 +20,7 @@ function implementsElementReferenceOrValue(arg: any): arg is ElementReferenceOrV arg.stringValue !== undefined || arg.numberValue !== undefined || arg.booleanValue !== undefined || + arg.dateValue !== undefined || arg.elementReference !== undefined ); } diff --git a/test/data/flow/after_save.json b/test/data/flow/after_save.json new file mode 100644 index 0000000..8ce3832 --- /dev/null +++ b/test/data/flow/after_save.json @@ -0,0 +1,28 @@ +{ + "fullName": "Start_Element", + "interviewLabel": "Start_Element {!$Flow.CurrentDateTime}", + "label": "Start_Element", + "processMetadataValues": [ + { + "name": "BuilderType", + "value": { + "stringValue": "LightningFlowBuilder" + } + }, + { + "name": "OriginBuilderType", + "value": { + "stringValue": "LightningFlowBuilder" + } + } + ], + "processType": "AutoLaunchedFlow", + "start": { + "locationX": "50", + "locationY": "50", + "object": "Case", + "recordTriggerType": "Update", + "triggerType": "RecordAfterSave" + }, + "status": "InvalidDraft" +} diff --git a/test/data/flow/before_save_create.json b/test/data/flow/before_save_create.json new file mode 100644 index 0000000..422e163 --- /dev/null +++ b/test/data/flow/before_save_create.json @@ -0,0 +1,28 @@ +{ + "fullName": "Start_Element", + "interviewLabel": "Start_Element {!$Flow.CurrentDateTime}", + "label": "Start_Element", + "processMetadataValues": [ + { + "name": "BuilderType", + "value": { + "stringValue": "LightningFlowBuilder" + } + }, + { + "name": "OriginBuilderType", + "value": { + "stringValue": "LightningFlowBuilder" + } + } + ], + "processType": "AutoLaunchedFlow", + "start": { + "locationX": "50", + "locationY": "50", + "object": "Case", + "recordTriggerType": "Create", + "triggerType": "RecordBeforeSave" + }, + "status": "InvalidDraft" +} diff --git a/test/data/flow/before_save_create_and_update.json b/test/data/flow/before_save_create_and_update.json new file mode 100644 index 0000000..10576f0 --- /dev/null +++ b/test/data/flow/before_save_create_and_update.json @@ -0,0 +1,28 @@ +{ + "fullName": "Start_Element", + "interviewLabel": "Start_Element {!$Flow.CurrentDateTime}", + "label": "Start_Element", + "processMetadataValues": [ + { + "name": "BuilderType", + "value": { + "stringValue": "LightningFlowBuilder" + } + }, + { + "name": "OriginBuilderType", + "value": { + "stringValue": "LightningFlowBuilder" + } + } + ], + "processType": "AutoLaunchedFlow", + "start": { + "locationX": "50", + "locationY": "50", + "object": "Case", + "recordTriggerType": "CreateAndUpdate", + "triggerType": "RecordBeforeSave" + }, + "status": "InvalidDraft" +} diff --git a/test/data/flow/before_save_update.json b/test/data/flow/before_save_update.json new file mode 100644 index 0000000..afa74c2 --- /dev/null +++ b/test/data/flow/before_save_update.json @@ -0,0 +1,28 @@ +{ + "fullName": "Start_Element", + "interviewLabel": "Start_Element {!$Flow.CurrentDateTime}", + "label": "Start_Element", + "processMetadataValues": [ + { + "name": "BuilderType", + "value": { + "stringValue": "LightningFlowBuilder" + } + }, + { + "name": "OriginBuilderType", + "value": { + "stringValue": "LightningFlowBuilder" + } + } + ], + "processType": "AutoLaunchedFlow", + "start": { + "locationX": "50", + "locationY": "50", + "object": "Case", + "recordTriggerType": "Update", + "triggerType": "RecordBeforeSave" + }, + "status": "InvalidDraft" +} diff --git a/test/lib/testFlow.json b/test/data/testFlow.json similarity index 92% rename from test/lib/testFlow.json rename to test/data/testFlow.json index 2722de6..90198c2 100644 --- a/test/lib/testFlow.json +++ b/test/data/testFlow.json @@ -2,7 +2,10 @@ "$": { "xmlns": "http://soap.sforce.com/2006/04/metadata" }, "actionCalls": [ { - "processMetadataValues": { "name": "emailAlertSelection", "value": { "stringValue": "Email_to_Financial_Group" } }, + "processMetadataValues": { + "name": "emailAlertSelection", + "value": { "stringValue": "Email_to_Financial_Group" } + }, "name": "myRule_1_A1", "label": "Email to Financial Group", "locationX": "100", @@ -24,10 +27,16 @@ "actionName": "submit", "actionType": "submit", "connector": { "targetReference": "myDecision5" }, - "inputParameters": [{ "name": "objectId", "value": { "elementReference": "myVariable_current.Id" } }, { "name": "comment" }] + "inputParameters": [ + { "name": "objectId", "value": { "elementReference": "myVariable_current.Id" } }, + { "name": "comment" } + ] }, { - "processMetadataValues": { "name": "emailAlertSelection", "value": { "stringValue": "Email_to_Financial_Group" } }, + "processMetadataValues": { + "name": "emailAlertSelection", + "value": { "stringValue": "Email_to_Financial_Group" } + }, "name": "myRule_6_A2", "label": "Email to Financial Group Again", "locationX": "600", @@ -43,7 +52,11 @@ "label": "myWaitAssignment_myWait_myRule_1", "locationX": "0", "locationY": "0", - "assignmentItems": { "assignToReference": "cancelWaits", "operator": "Add", "value": { "stringValue": "myWait_myRule_1" } }, + "assignmentItems": { + "assignToReference": "cancelWaits", + "operator": "Add", + "value": { "stringValue": "myWait_myRule_1" } + }, "connector": { "targetReference": "myDecision" } }, { @@ -123,7 +136,11 @@ "rules": { "name": "myRule_6", "conditionLogic": "and", - "conditions": { "leftValueReference": "formula_myRule_6", "operator": "EqualTo", "rightValue": { "booleanValue": "true" } }, + "conditions": { + "leftValueReference": "formula_myRule_6", + "operator": "EqualTo", + "rightValue": { "booleanValue": "true" } + }, "connector": { "targetReference": "myRule_6_A1" }, "label": "closed Won?" } @@ -138,7 +155,11 @@ "rules": { "name": "myRule_9", "conditionLogic": "and", - "conditions": { "leftValueReference": "formula_myRule_9", "operator": "EqualTo", "rightValue": { "booleanValue": "true" } }, + "conditions": { + "leftValueReference": "formula_myRule_9", + "operator": "EqualTo", + "rightValue": { "booleanValue": "true" } + }, "label": "All OK" } }, @@ -147,7 +168,9 @@ "label": "myPostWaitDecision_myWaitEvent_myWait_myRule_1_event_0", "locationX": "0", "locationY": "0", - "defaultConnector": { "targetReference": "myWaitEvent_myWait_myRule_1_event_0_postWaitExecutionAssignment" }, + "defaultConnector": { + "targetReference": "myWaitEvent_myWait_myRule_1_event_0_postWaitExecutionAssignment" + }, "defaultConnectorLabel": "default", "rules": { "name": "myPostWaitRule_myWaitEvent_myWait_myRule_1_event_0", @@ -197,7 +220,11 @@ { "name": "myRule_1_pmetnullrule", "conditionLogic": "or", - "conditions": { "leftValueReference": "myVariable_old", "operator": "IsNull", "rightValue": { "booleanValue": "true" } }, + "conditions": { + "leftValueReference": "myVariable_old", + "operator": "IsNull", + "rightValue": { "booleanValue": "true" } + }, "connector": { "targetReference": "myRule_1_A1" }, "label": "Previously Met - Null" }, @@ -229,7 +256,10 @@ "expression": "TODAY() + 7" }, { - "processMetadataValues": { "name": "originalFormula", "value": { "stringValue": "180 + [Opportunity].CloseDate " } }, + "processMetadataValues": { + "name": "originalFormula", + "value": { "stringValue": "180 + [Opportunity].CloseDate " } + }, "name": "formula_7_myRule_6_A1_6831756980", "dataType": "Date", "expression": "180 + {!myVariable_current.CloseDate}" diff --git a/test/lib/converter/readableFlowStart.test.ts b/test/lib/converter/readableFlowStart.test.ts new file mode 100644 index 0000000..7984b73 --- /dev/null +++ b/test/lib/converter/readableFlowStart.test.ts @@ -0,0 +1,34 @@ +import beforeSaveCreate from '../../data/flow/before_save_create.json'; +import beforeSaveUpdate from '../../data/flow/before_save_update.json'; +import beforeSaveCreateAndUpdate from '../../data/flow/before_save_create_and_update.json'; +import afterSave from '../../data/flow/after_save.json'; +import ReadableFlowStartElement from '../../../src/lib/converter/helper/readableFlowStart'; + +describe('lib/converter/readableFlowStart', () => { + it('Before save flow (create)', () => { + const start = new ReadableFlowStartElement(beforeSaveCreate).getReadableElement(); + expect(start.triggerType).toBe('FLOW_TRIGGER_RECORD'); + expect(start.recordTriggerType).toBe('FROW_TRIGGER_RECORD_CREATE_ONLY'); + expect(start.context).toBe('BEFORE_SAVE'); + }); + + it('Before save flow (update)', () => { + const start = new ReadableFlowStartElement(beforeSaveUpdate).getReadableElement(); + expect(start.triggerType).toBe('FLOW_TRIGGER_RECORD'); + expect(start.recordTriggerType).toBe('FROW_TRIGGER_RECORD_UPDATE_ONLY'); + expect(start.context).toBe('BEFORE_SAVE'); + }); + + it('Before save flow (create and update)', () => { + const start = new ReadableFlowStartElement(beforeSaveCreateAndUpdate).getReadableElement(); + expect(start.triggerType).toBe('FLOW_TRIGGER_RECORD'); + expect(start.recordTriggerType).toBe('FROW_TRIGGER_RECORD_CREATE_OR_UPDATE'); + expect(start.context).toBe('BEFORE_SAVE'); + }); + + it('After save flow', () => { + const start = new ReadableFlowStartElement(afterSave).getReadableElement(); + expect(start.triggerType).toBe('FLOW_TRIGGER_RECORD'); + expect(start.context).toBe('AFTER_SAVE'); + }); +}); diff --git a/test/lib/docx/docxBuilder.test.ts b/test/lib/docx/docxBuilder.test.ts index e00c2fa..18b253b 100644 --- a/test/lib/docx/docxBuilder.test.ts +++ b/test/lib/docx/docxBuilder.test.ts @@ -1,15 +1,14 @@ import { Document } from 'docx'; -import buildDocxContent from '../../../src/lib/docx/docxBuilder'; -import FlowParser from '../../../src/lib/flowParser'; -import { Flow } from '../../../src/types/flow'; +import { Flow } from '../../../src/types/metadata/flow'; -import testFlow from '../testFlow.json'; +import testFlow from '../../data/testFlow.json'; +import DocxBuilder from '../../../src/lib/docx/docxBuilder'; +import { ReadableProcessMetadataConverter } from '../../../src/lib/converter/metadataConverter'; describe('lib/pdf/pdfBuilder', () => { it('buildPdfContent()', () => { - const fp = new FlowParser(testFlow as Flow, 'test_flow'); - const readableFlow = fp.createReadableProcess(); - const docxContent = buildDocxContent(readableFlow, 'en'); + const docxBuilder = new DocxBuilder('en'); + const docxContent = new ReadableProcessMetadataConverter(testFlow as Flow, 'test_flow').accept(docxBuilder); expect(docxContent instanceof Document).toBeTruthy(); }); }); diff --git a/test/lib/flowParser.test.ts b/test/lib/flowParser.test.ts deleted file mode 100644 index 0aa3906..0000000 --- a/test/lib/flowParser.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import testFlow from './testFlow.json'; -import FlowParser from '../../src/lib/flowParser'; -import { Flow } from '../../src/types/flow'; - -describe('lib/flowParser', () => { - let flowParser; - - beforeAll(() => { - flowParser = new FlowParser(testFlow as Flow, 'test_flow'); - }); - - it('supported flow', () => { - expect(flowParser.isSupportedFlow()).toBeTruthy(); - }); - - it('readable flow creation', () => { - const readableProcess = flowParser.createReadableProcess(); - expect(readableProcess.eventMatchingConditions).toBeFalsy(); - expect(readableProcess.actionGroups).toHaveLength(4); - }); -}); diff --git a/test/lib/json/jsonBuilder.test.ts b/test/lib/json/jsonBuilder.test.ts new file mode 100644 index 0000000..ad92dd4 --- /dev/null +++ b/test/lib/json/jsonBuilder.test.ts @@ -0,0 +1,13 @@ +import { Flow } from '../../../src/types/metadata/flow'; + +import testFlow from '../../data/testFlow.json'; +import JsonBuilder from '../../../src/lib/json/jsonBuilder'; +import { ReadableProcessMetadataConverter } from '../../../src/lib/converter/metadataConverter'; + +describe('lib/pdf/pdfBuilder', () => { + it('buildPdfContent()', () => { + const jsonBuilder = new JsonBuilder('en'); + const jsonContent = new ReadableProcessMetadataConverter(testFlow as Flow, 'test_flow').accept(jsonBuilder); + expect(jsonContent).toBeTruthy(); + }); +}); diff --git a/test/lib/pdf/pdfBuilder.test.ts b/test/lib/pdf/pdfBuilder.test.ts index ce3170b..6cead6b 100644 --- a/test/lib/pdf/pdfBuilder.test.ts +++ b/test/lib/pdf/pdfBuilder.test.ts @@ -1,14 +1,13 @@ -import buildPdfContent from '../../../src/lib/pdf/pdfBuilder'; -import FlowParser from '../../../src/lib/flowParser'; -import { Flow } from '../../../src/types/flow'; +import { Flow } from '../../../src/types/metadata/flow'; -import testFlow from '../testFlow.json'; +import testFlow from '../../data/testFlow.json'; +import PdfBuilder from '../../../src/lib/pdf/pdfBuilder'; +import { ReadableProcessMetadataConverter } from '../../../src/lib/converter/metadataConverter'; describe('lib/pdf/pdfBuilder', () => { it('buildPdfContent()', () => { - const fp = new FlowParser(testFlow as Flow, 'test_flow'); - const readableFlow = fp.createReadableProcess(); - const pdfContent = buildPdfContent(readableFlow, 'en'); + const pdfBuilder = new PdfBuilder('en'); + const pdfContent = new ReadableProcessMetadataConverter(testFlow as Flow, 'test_flow').accept(pdfBuilder); expect(pdfContent.content).toHaveLength(48); }); }); diff --git a/test/lib/arrayUtils.test.ts b/test/lib/util/arrayUtils.test.ts similarity index 81% rename from test/lib/arrayUtils.test.ts rename to test/lib/util/arrayUtils.test.ts index 0812736..06d5928 100644 --- a/test/lib/arrayUtils.test.ts +++ b/test/lib/util/arrayUtils.test.ts @@ -1,4 +1,4 @@ -import { toArray } from '../../src/lib/util/arrayUtils'; +import { toArray } from '../../../src/lib/util/arrayUtils'; describe('lib/arrayUtils', () => { it('toArray()', () => { diff --git a/test/lib/util/flowUtils.test.ts b/test/lib/util/flowUtils.test.ts new file mode 100644 index 0000000..61f5b5c --- /dev/null +++ b/test/lib/util/flowUtils.test.ts @@ -0,0 +1,13 @@ +import testFlow from '../../data/testFlow.json'; +import { Flow } from '../../../src/types/metadata/flow'; +import { isSupported, isProcess } from '../../../src/lib/util/flowUtils'; + +describe('lib/flowUtils', () => { + it('isSupported', () => { + expect(isSupported(testFlow as Flow)).toBeTruthy(); + }); + + it('isProcess', () => { + expect(isProcess(testFlow as Flow)).toBeTruthy(); + }); +}); diff --git a/test/lib/stringUtils.test.ts b/test/lib/util/stringUtils.test.ts similarity index 83% rename from test/lib/stringUtils.test.ts rename to test/lib/util/stringUtils.test.ts index 03d2c8f..ea5124d 100644 --- a/test/lib/stringUtils.test.ts +++ b/test/lib/util/stringUtils.test.ts @@ -1,4 +1,4 @@ -import { toUpperSnakeCase } from '../../src/lib/util/stringUtils'; +import { toUpperSnakeCase } from '../../../src/lib/util/stringUtils'; describe('lib/utils/stringUtils', () => { it('toUpperSnakeCase()', () => {