Track HiveGrid Cloud planning reference

This commit is contained in:
david committed 2026-06-26 20:48:01 +02:00
1 parent b3d5ea6764
commit e341c5938a
5784 files changed
+1104929 -3

No files matched your search

@@ -0,0 +1,717 @@
import { WorkflowsTemplateDto } from "../dtos/WorkflowsTemplateDto";
type LanguageScopedWorkflowsTemplateDto = WorkflowsTemplateDto & {
defaultLanguage: "en";
languages: readonly ["en"];
};
const ENGLISH_TEMPLATE_SCOPE = {
defaultLanguage: "en",
languages: ["en"],
} as const;
function templateKey(path: string) {
return `workflows.defaultTemplates.${path}`;
}
const templates: LanguageScopedWorkflowsTemplateDto[] = [
{
...ENGLISH_TEMPLATE_SCOPE,
adminOnly: true,
key: "sampleHttpRequests",
title: templateKey("sampleHttpRequests.title"),
workflows: [
{
key: "getJsonPlaceholderTodoItem",
name: templateKey("sampleHttpRequests.workflows.getJsonPlaceholderTodoItem.name"),
description: templateKey("sampleHttpRequests.workflows.getJsonPlaceholderTodoItem.description"),
blocks: [
{
id: "manualTrigger",
type: "manual",
description: templateKey("sampleHttpRequests.workflows.getJsonPlaceholderTodoItem.blocks.manualTrigger.description"),
input: {
validation: JSON.stringify(
{
type: "object",
properties: {
id: { type: "number" },
},
required: ["id"],
},
null,
2
),
},
},
{
id: "httpRequest",
type: "httpRequest",
description: templateKey("sampleHttpRequests.workflows.getJsonPlaceholderTodoItem.blocks.httpRequest.description"),
input: {
url: "{{$vars.jsonPlaceholderUrl}}/todos/{{$params.id}}",
method: "GET",
// body: "",
// headers: {},
throwsError: true,
},
},
{
id: "if",
type: "if",
description: templateKey("sampleHttpRequests.workflows.getJsonPlaceholderTodoItem.blocks.if.description"),
conditionGroups: [
{
type: "AND",
conditions: [
{
variable: "{{httpRequest.statusCode}}",
operator: "=",
value: "200",
},
],
},
],
},
{
id: "logNotFound",
type: "alertUser",
description: templateKey("sampleHttpRequests.workflows.getJsonPlaceholderTodoItem.blocks.logNotFound.description"),
input: {
type: "error",
message: templateKey("sampleHttpRequests.workflows.getJsonPlaceholderTodoItem.blocks.logNotFound.input.message"),
},
},
{
id: "logFound",
type: "alertUser",
description: templateKey("sampleHttpRequests.workflows.getJsonPlaceholderTodoItem.blocks.logFound.description"),
input: {
message: templateKey("sampleHttpRequests.workflows.getJsonPlaceholderTodoItem.blocks.logFound.input.message"),
},
},
{
id: "switchTitle",
type: "switch",
description: templateKey("sampleHttpRequests.workflows.getJsonPlaceholderTodoItem.blocks.switchTitle.description"),
conditionGroups: [
{
type: "AND",
case: "case1",
conditions: [
{
variable: "{{httpRequest.body.title}}",
operator: "=",
value: "delectus aut autem",
},
],
},
{
type: "AND",
case: "case2",
conditions: [
{
variable: "{{httpRequest.body.title}}",
operator: "contains",
value: "quis",
},
],
},
],
},
{
id: "switchCase1",
type: "alertUser",
description: templateKey("sampleHttpRequests.workflows.getJsonPlaceholderTodoItem.blocks.switchCase1.description"),
input: {
message: templateKey("sampleHttpRequests.workflows.getJsonPlaceholderTodoItem.blocks.switchCase1.input.message"),
},
},
{
id: "switchCase2",
type: "alertUser",
description: templateKey("sampleHttpRequests.workflows.getJsonPlaceholderTodoItem.blocks.switchCase2.description"),
input: {
message: templateKey("sampleHttpRequests.workflows.getJsonPlaceholderTodoItem.blocks.switchCase2.input.message"),
},
},
{
id: "switchDefault",
type: "alertUser",
description: templateKey("sampleHttpRequests.workflows.getJsonPlaceholderTodoItem.blocks.switchDefault.description"),
input: {
message: templateKey("sampleHttpRequests.workflows.getJsonPlaceholderTodoItem.blocks.switchDefault.input.message"),
},
},
{
id: "alertUser",
type: "alertUser",
description: templateKey("sampleHttpRequests.workflows.getJsonPlaceholderTodoItem.blocks.alertUser.description"),
input: {
message: templateKey("sampleHttpRequests.workflows.getJsonPlaceholderTodoItem.blocks.alertUser.input.message"),
},
},
],
toBlocks: [
{ fromBlockId: "manualTrigger", toBlockId: "httpRequest" },
{ fromBlockId: "httpRequest", toBlockId: "if" },
{ fromBlockId: "if", toBlockId: "logNotFound", condition: "false" },
{ fromBlockId: "if", toBlockId: "logFound", condition: "true" },
{ fromBlockId: "logFound", toBlockId: "switchTitle" },
{ fromBlockId: "switchTitle", toBlockId: "switchCase1", condition: "case1" },
{ fromBlockId: "switchTitle", toBlockId: "switchCase2", condition: "case2" },
{ fromBlockId: "switchTitle", toBlockId: "switchDefault", condition: "default" },
{ fromBlockId: "switchCase1", toBlockId: "alertUser" },
{ fromBlockId: "switchCase2", toBlockId: "alertUser" },
{ fromBlockId: "switchDefault", toBlockId: "alertUser" },
],
inputExamples: [
{
key: "existingItem",
title: templateKey("sampleHttpRequests.workflows.getJsonPlaceholderTodoItem.inputExamples.existingItem.title"),
input: { id: 10 },
},
{
key: "nonExistingItem",
title: templateKey("sampleHttpRequests.workflows.getJsonPlaceholderTodoItem.inputExamples.nonExistingItem.title"),
input: { id: -1 },
},
{
key: "idParameterNotProvided",
title: templateKey("sampleHttpRequests.workflows.getJsonPlaceholderTodoItem.inputExamples.idParameterNotProvided.title"),
input: {},
},
{
key: "idParameterNotNumber",
title: templateKey("sampleHttpRequests.workflows.getJsonPlaceholderTodoItem.inputExamples.idParameterNotNumber.title"),
input: { id: "abc" },
},
{
key: "case1Equal",
title: templateKey("sampleHttpRequests.workflows.getJsonPlaceholderTodoItem.inputExamples.case1Equal.title"),
input: { id: 1 },
},
{
key: "case2Contains",
title: templateKey("sampleHttpRequests.workflows.getJsonPlaceholderTodoItem.inputExamples.case2Contains.title"),
input: { id: 2 },
},
{
key: "defaultCase",
title: templateKey("sampleHttpRequests.workflows.getJsonPlaceholderTodoItem.inputExamples.defaultCase.title"),
input: { id: 3 },
},
],
},
],
variables: [{ name: "jsonPlaceholderUrl", value: "https://jsonplaceholder.typicode.com" }],
},
{
...ENGLISH_TEMPLATE_SCOPE,
adminOnly: true,
key: "alertUser",
title: templateKey("alertUser.title"),
workflows: [
{
key: "alertUser",
name: templateKey("alertUser.workflows.alertUser.name"),
description: templateKey("alertUser.workflows.alertUser.description"),
blocks: [
{
id: "manualTrigger",
type: "manual",
description: templateKey("alertUser.workflows.alertUser.blocks.manualTrigger.description"),
input: {
validation: JSON.stringify(
{
type: "object",
properties: {
message: { type: "string" },
},
required: ["message"],
},
null,
2
),
},
},
{
id: "alertUser",
type: "alertUser",
description: templateKey("alertUser.workflows.alertUser.blocks.alertUser.description"),
input: {
message: `{{$params.message}}`,
},
},
],
toBlocks: [{ fromBlockId: "manualTrigger", toBlockId: "alertUser" }],
inputExamples: [
{ key: "existingItem", title: templateKey("alertUser.workflows.alertUser.inputExamples.existingItem.title"), input: { message: "Hello world!" } },
],
},
],
variables: [],
},
{
...ENGLISH_TEMPLATE_SCOPE,
adminOnly: true,
key: "iterator",
title: templateKey("iterator.title"),
workflows: [
{
key: "loopItems",
name: templateKey("iterator.workflows.loopItems.name"),
description: templateKey("iterator.workflows.loopItems.description"),
blocks: [
{
id: "iterator",
type: "iterator",
description: templateKey("iterator.workflows.loopItems.blocks.iterator.description"),
input: {
variableName: "{{$params.array}}",
},
conditionGroups: [],
},
{
id: "manual",
type: "manual",
description: templateKey("iterator.workflows.loopItems.blocks.manual.description"),
input: {},
conditionGroups: [],
},
{
id: "log1",
type: "log",
description: templateKey("iterator.workflows.loopItems.blocks.log1.description"),
input: {
message: templateKey("iterator.workflows.loopItems.blocks.log1.input.message"),
},
conditionGroups: [],
},
{
id: "log2",
type: "log",
description: templateKey("iterator.workflows.loopItems.blocks.log2.description"),
input: {
message: templateKey("iterator.workflows.loopItems.blocks.log2.input.message"),
},
conditionGroups: [],
},
],
toBlocks: [
{
fromBlockId: "iterator",
toBlockId: "log1",
condition: "loopNext",
},
{
fromBlockId: "iterator",
toBlockId: "log2",
condition: "loopEnd",
},
{
fromBlockId: "manual",
toBlockId: "iterator",
},
],
inputExamples: [
{
key: "stringArray",
title: templateKey("iterator.workflows.loopItems.inputExamples.stringArray.title"),
input: {
array: ["value 1", "value 2"],
},
},
{
key: "objectArray",
title: templateKey("iterator.workflows.loopItems.inputExamples.objectArray.title"),
input: {
array: [
{
name: "Value 1",
value: "value 1",
},
{
name: "Value 2",
value: "value 2",
},
],
},
},
],
},
],
variables: [],
credentialsRequired: [],
},
{
...ENGLISH_TEMPLATE_SCOPE,
key: "rowEventsEmployee",
title: templateKey("rowEventsEmployee.title"),
workflows: [
// {
// name: "Row Get: Employee",
// description: "Gets employee or null",
// blocks: [
// {
// id: "manual",
// type: "manual",
// input: {},
// conditionGroups: [],
// },
// {
// id: "rowGet",
// type: "rowGet",
// input: {
// entity: "employee",
// id: "{{$params.id}}",
// },
// conditionGroups: [],
// },
// {
// id: "if",
// type: "if",
// input: {},
// conditionGroups: [
// {
// type: "AND",
// conditions: [
// {
// variable: "{{rowGet.row}}",
// operator: "isNotEmpty",
// value: "",
// },
// ],
// },
// ],
// },
// {
// id: "log1",
// type: "log",
// description: "Found",
// input: {
// message:
// "Employee: {{rowGet.row.firstName}}. Last name: {{rowGet.row.lastName}}. Email: {{rowGet.row.email}}.Employee: {{rowGet.row.firstName}}. Last name: {{rowGet.row.lastName}}. Email: {{rowGet.row.email}}.",
// },
// conditionGroups: [],
// },
// {
// id: "log2",
// type: "log",
// description: "Not found",
// input: {
// message: "Not found",
// },
// conditionGroups: [],
// },
// ],
// toBlocks: [
// {
// fromBlockId: "manual",
// toBlockId: "rowGet",
// },
// {
// fromBlockId: "rowGet",
// toBlockId: "if",
// },
// {
// fromBlockId: "if",
// toBlockId: "log1",
// condition: "true",
// },
// {
// fromBlockId: "if",
// toBlockId: "log2",
// condition: "false",
// },
// ],
// inputExamples: [
// {
// title: "Valid request",
// input: {
// id: "ROW_ID_HERE",
// },
// },
// ],
// },
{
key: "createdEmployee",
name: templateKey("rowEventsEmployee.workflows.createdEmployee.name"),
blocks: [
{
id: "event",
type: "event",
description: templateKey("rowEventsEmployee.workflows.createdEmployee.blocks.event.description"),
input: {
event: "row.created",
entity: "employee",
},
conditionGroups: [],
},
{
id: "log",
type: "log",
description: templateKey("rowEventsEmployee.workflows.createdEmployee.blocks.log.description"),
input: {
message: templateKey("rowEventsEmployee.workflows.createdEmployee.blocks.log.input.message"),
},
conditionGroups: [],
},
],
toBlocks: [
{
fromBlockId: "event",
toBlockId: "log",
},
],
inputExamples: [
{
key: "fakeEmployee",
title: templateKey("rowEventsEmployee.workflows.createdEmployee.inputExamples.fakeEmployee.title"),
input: {
event: "created",
entity: "employee",
row: {
id: "fake-id",
firstName: "Avery",
lastName: "Morgan",
email: "avery.morgan@example.com",
},
},
},
{
key: "invalidEvent",
title: templateKey("rowEventsEmployee.workflows.createdEmployee.inputExamples.invalidEvent.title"),
input: {
event: "updated",
entity: "employee",
row: {
id: "fake-id",
firstName: "Avery",
lastName: "Morgan",
email: "avery.morgan@example.com",
},
},
},
{
key: "invalidEntity",
title: templateKey("rowEventsEmployee.workflows.createdEmployee.inputExamples.invalidEntity.title"),
input: {
event: "created",
entity: "sale",
row: {
id: "fake-id",
},
},
},
],
},
// {
// name: "Row Create: Employee",
// description: "Creates John Doe employee",
// blocks: [
// {
// id: "rowCreate",
// type: "rowCreate",
// description: "Creates John Doe employee",
// input: {
// entity: "employee",
// row: '{\n "firstName": "John",\n "lastName": "Doe",\n "email": "john@doe.com"\n}',
// },
// conditionGroups: [],
// },
// {
// id: "manual",
// type: "manual",
// input: {},
// conditionGroups: [],
// },
// ],
// toBlocks: [
// {
// fromBlockId: "manual",
// toBlockId: "rowCreate",
// },
// ],
// inputExamples: [],
// },
{
key: "updatedEmployee",
name: templateKey("rowEventsEmployee.workflows.updatedEmployee.name"),
description: templateKey("rowEventsEmployee.workflows.updatedEmployee.description"),
blocks: [
{
id: "event",
type: "event",
description: templateKey("rowEventsEmployee.workflows.updatedEmployee.blocks.event.description"),
input: {
event: "row.updated",
entity: "employee",
},
conditionGroups: [],
},
{
id: "log",
type: "log",
description: templateKey("rowEventsEmployee.workflows.updatedEmployee.blocks.log.description"),
input: {
message: templateKey("rowEventsEmployee.workflows.updatedEmployee.blocks.log.input.message"),
},
conditionGroups: [],
},
],
toBlocks: [
{
fromBlockId: "event",
toBlockId: "log",
},
],
inputExamples: [],
},
// {
// name: "Row Update: Employee",
// description: "Updates employee",
// blocks: [
// {
// id: "manual",
// type: "manual",
// input: {},
// conditionGroups: [],
// },
// {
// id: "rowUpdate",
// type: "rowUpdate",
// input: {
// data: '{\n "firstName": "Updated first name",\n "lastName": "Updated last name"\n}',
// id: "{{$params.id}}",
// entity: "employee",
// },
// conditionGroups: [],
// },
// ],
// toBlocks: [
// {
// fromBlockId: "manual",
// toBlockId: "rowUpdate",
// },
// ],
// inputExamples: [
// {
// title: "Valid request",
// input: {
// id: "ROW_ID_HERE",
// },
// },
// ],
// },
{
key: "deletedEmployee",
name: templateKey("rowEventsEmployee.workflows.deletedEmployee.name"),
description: templateKey("rowEventsEmployee.workflows.deletedEmployee.description"),
blocks: [
{
id: "log",
type: "log",
description: templateKey("rowEventsEmployee.workflows.deletedEmployee.blocks.log.description"),
input: {
message: templateKey("rowEventsEmployee.workflows.deletedEmployee.blocks.log.input.message"),
},
conditionGroups: [],
},
{
id: "event",
type: "event",
description: templateKey("rowEventsEmployee.workflows.deletedEmployee.blocks.event.description"),
input: {
event: "row.deleted",
entity: "employee",
},
conditionGroups: [],
},
],
toBlocks: [
{
fromBlockId: "event",
toBlockId: "log",
},
],
inputExamples: [],
},
// {
// name: "Row Delete: Employee",
// description: "Deletes employee",
// blocks: [
// {
// id: "manual",
// type: "manual",
// input: {},
// conditionGroups: [],
// },
// {
// id: "rowDelete",
// type: "rowDelete",
// input: {
// entity: "employee",
// id: "{{$params.id}}",
// },
// conditionGroups: [],
// },
// ],
// toBlocks: [
// {
// fromBlockId: "manual",
// toBlockId: "rowDelete",
// },
// ],
// inputExamples: [
// {
// title: "Valid request",
// input: {
// id: "ROW_ID_HERE",
// },
// },
// ],
// },
],
},
{
...ENGLISH_TEMPLATE_SCOPE,
key: "emails",
title: templateKey("emails.title"),
workflows: [
{
key: "mailjetEmail",
name: templateKey("emails.workflows.mailjetEmail.name"),
blocks: [
{
id: "manual",
type: "manual",
input: {},
conditionGroups: [],
},
{
id: "email",
type: "email",
description: templateKey("emails.workflows.mailjetEmail.blocks.email.description"),
input: {
provider: "mailjet",
to: "{{$session.user.email}}",
subject: templateKey("emails.workflows.mailjetEmail.blocks.email.input.subject"),
body: templateKey("emails.workflows.mailjetEmail.blocks.email.input.body"),
},
conditionGroups: [],
},
],
toBlocks: [
{
fromBlockId: "manual",
toBlockId: "email",
},
],
inputExamples: [],
},
],
},
];
export default templates;
@@ -0,0 +1,36 @@
import { describe, expect, it } from "vitest";
import { formatWorkflowExecutionErrorText } from "./workflowExecutionErrorText";
function t(key: string, options?: Record<string, unknown>) {
return JSON.stringify({ key, options });
}
describe("formatWorkflowExecutionErrorText", () => {
it("maps exact workflow runtime errors to translatable keys", () => {
expect(formatWorkflowExecutionErrorText("URL is required", t)).toBe(
JSON.stringify({
key: "workflows.executionErrors.urlRequired",
options: { defaultValue: "URL is required" },
})
);
});
it("maps parameterized workflow runtime errors to translatable keys", () => {
expect(formatWorkflowExecutionErrorText('Variable "orders" is not an array', t)).toBe(
JSON.stringify({
key: "workflows.executionErrors.variableNotArray",
options: { name: "orders", defaultValue: 'Variable "orders" is not an array' },
})
);
});
it("keeps unknown provider errors unchanged", () => {
expect(formatWorkflowExecutionErrorText("Provider failed for https://api.example.test", t)).toBe("Provider failed for https://api.example.test");
});
it("returns an empty string for empty errors", () => {
expect(formatWorkflowExecutionErrorText(null, t)).toBe("");
expect(formatWorkflowExecutionErrorText(undefined, t)).toBe("");
});
});
@@ -0,0 +1,83 @@
type WorkflowErrorT = (key: string, options?: Record<string, unknown>) => string;
type WorkflowErrorRule = {
key: string;
match: string | RegExp;
params?: (match: RegExpMatchArray) => Record<string, string>;
};
const WORKFLOW_ERROR_RULES: WorkflowErrorRule[] = [
{ match: "Workflow is not ready to run", key: "workflows.executionErrors.workflowNotReady" },
{ match: /^Invalid workflow block type: (.+)$/u, key: "workflows.executionErrors.invalidBlockType", params: ([, type]) => ({ type }) },
{ match: /^Workflow execution cycle detected at block (.+)$/u, key: "workflows.executionErrors.cycleDetected", params: ([, blockId]) => ({ blockId }) },
{ match: /^Workflow execution exceeded maximum depth of (.+)$/u, key: "workflows.executionErrors.maxDepthExceeded", params: ([, depth]) => ({ depth }) },
{ match: "URL is required", key: "workflows.executionErrors.urlRequired" },
{ match: "Method is required", key: "workflows.executionErrors.methodRequired" },
{ match: "Headers must be a JSON object", key: "workflows.executionErrors.headersJsonObject" },
{ match: "Missing true block", key: "workflows.executionErrors.missingTrueBlock" },
{ match: "Missing false block", key: "workflows.executionErrors.missingFalseBlock" },
{ match: "If block must have exactly one condition group", key: "workflows.executionErrors.ifSingleConditionGroup" },
{
match: /^\[(.+)\] Missing block for condition: (.+)$/u,
key: "workflows.executionErrors.missingConditionBlock",
params: ([, blockName, condition]) => ({ blockName, condition }),
},
{ match: "Missing default block", key: "workflows.executionErrors.missingDefaultBlock" },
{
match: /^\[(.+)\] Missing loop next block$/u,
key: "workflows.executionErrors.missingLoopNextBlock",
params: ([, blockName]) => ({ blockName }),
},
{
match: /^\[(.+)\] Missing loop end block$/u,
key: "workflows.executionErrors.missingLoopEndBlock",
params: ([, blockName]) => ({ blockName }),
},
{ match: /^Property not found for key: (.+)$/u, key: "workflows.executionErrors.propertyNotFound", params: ([, key]) => ({ key }) },
{ match: /^Variable "(.+)" not found$/u, key: "workflows.executionErrors.variableNotFound", params: ([, name]) => ({ name }) },
{ match: /^Variable "(.+)" is not an array$/u, key: "workflows.executionErrors.variableNotArray", params: ([, name]) => ({ name }) },
{ match: "Variable blocks cannot persist workflow credentials.", key: "workflows.executionErrors.variableCredentialPersistence" },
{ match: /^Invalid operator: (.+)$/u, key: "workflows.executionErrors.invalidOperator", params: ([, operator]) => ({ operator }) },
{ match: /^Block type not implemented: (.+)$/u, key: "workflows.executionErrors.blockTypeNotImplemented", params: ([, type]) => ({ type }) },
{ match: "No response from AI", key: "workflows.executionErrors.noAiResponse" },
{ match: "Missing email data: provider, to, subject, body", key: "workflows.executionErrors.missingEmailData" },
{ match: /^Invalid email provider: (.+)$/u, key: "workflows.executionErrors.invalidEmailProvider", params: ([, provider]) => ({ provider }) },
{ match: /^Workflow not found: (.+)$/u, key: "workflows.executionErrors.workflowNotFoundWithId", params: ([, workflowId]) => ({ workflowId }) },
{ match: "Workflow is not live.", key: "workflows.executionErrors.workflowNotLive" },
{ match: "Workflow is not available for tenant API execution.", key: "workflows.executionErrors.workflowTenantApiUnavailable" },
{ match: /^Workflow has no block with id (.+)$/u, key: "workflows.executionErrors.workflowMissingBlockWithId", params: ([, blockId]) => ({ blockId }) },
{ match: "Workflow has no trigger block", key: "workflows.executionErrors.workflowMissingTriggerBlock" },
{ match: "Workflow execution not found", key: "workflows.executionErrors.executionNotFound" },
{ match: "Workflow execution is not waiting for a block", key: "workflows.executionErrors.executionNotWaitingForBlock" },
{ match: "Workflow execution is not waiting", key: "workflows.executionErrors.executionNotWaiting" },
{
match: /^Workflow execution does not belong to workflow (.+)$/u,
key: "workflows.executionErrors.executionWorkflowMismatch",
params: ([, workflowId]) => ({ workflowId }),
},
{ match: "Workflow execution type mismatch", key: "workflows.executionErrors.executionTypeMismatch" },
{ match: "Workflow execution is not queued", key: "workflows.executionErrors.executionNotQueued" },
{ match: "Workflow stream execution failed.", key: "workflows.executionErrors.streamFailed" },
];
export function formatWorkflowExecutionErrorText(error: string | null | undefined, t: WorkflowErrorT): string {
if (!error) {
return "";
}
for (const rule of WORKFLOW_ERROR_RULES) {
if (typeof rule.match === "string") {
if (rule.match === error) {
return t(rule.key, { defaultValue: error });
}
continue;
}
const match = error.match(rule.match);
if (match) {
return t(rule.key, { ...rule.params?.(match), defaultValue: error });
}
}
return error;
}
@@ -0,0 +1,209 @@
import fs from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import type { TFunction } from "i18next";
import DefaultWorkflowTemplates from "./DefaultWorkflowTemplates";
import {
getTemplateBlockDescription,
getTemplateInputExampleTitle,
getTemplateTitle,
getTemplateWorkflowDescription,
getTemplateWorkflowName,
localizeTemplateInputExampleInput,
localizeTemplateBlockInput,
localizeWorkflowTemplate,
} from "./workflowTemplateI18n";
import type { WorkflowsTemplateDto } from "../dtos/WorkflowsTemplateDto";
const template: WorkflowsTemplateDto = {
key: "sample",
title: "Sample",
workflows: [
{
key: "workflow",
name: "Workflow",
description: "Description",
blocks: [{ id: "manual", type: "manual", description: "Manual block", input: { message: "Hello", url: "https://example.com" } }],
toBlocks: [],
inputExamples: [{ key: "example", title: "Example", input: { message: "Hello", nested: { label: "Label" }, items: ["One"], array: ["Array item", { name: "Name", value: "Value" }] } }],
},
],
};
const t: TFunction = ((key: string, options?: { defaultValue?: string }) => `${key}::${options?.defaultValue ?? ""}`) as TFunction;
const repoRoot = process.cwd();
describe("workflowTemplateI18n", () => {
it("builds semantic keys for default workflow template metadata", () => {
const workflow = template.workflows[0];
expect(getTemplateTitle(template, t)).toBe("workflows.defaultTemplates.sample.title::Sample");
expect(getTemplateWorkflowName(template, workflow, t)).toBe("workflows.defaultTemplates.sample.workflows.workflow.name::Workflow");
expect(getTemplateWorkflowDescription(template, workflow, t)).toBe("workflows.defaultTemplates.sample.workflows.workflow.description::Description");
expect(getTemplateBlockDescription(template, workflow, workflow.blocks[0], t)).toBe(
"workflows.defaultTemplates.sample.workflows.workflow.blocks.manual.description::Manual block"
);
expect(getTemplateInputExampleTitle(template, workflow, workflow.inputExamples[0], t)).toBe(
"workflows.defaultTemplates.sample.workflows.workflow.inputExamples.example.title::Example"
);
});
it("localizes only user-facing template block input fields", () => {
const workflow = template.workflows[0];
const localized = localizeTemplateBlockInput(template, workflow, workflow.blocks[0], t);
expect(localized).toEqual({
message: "workflows.defaultTemplates.sample.workflows.workflow.blocks.manual.input.message::Hello",
url: "https://example.com",
});
});
it("localizes keyed input example string payloads recursively", () => {
const workflow = template.workflows[0];
const input = localizeTemplateInputExampleInput(template, workflow, workflow.inputExamples[0], t);
expect(input).toEqual({
message: "workflows.defaultTemplates.sample.workflows.workflow.inputExamples.example.input.message::Hello",
nested: {
label: "Label",
},
items: ["One"],
array: [
"workflows.defaultTemplates.sample.workflows.workflow.inputExamples.example.input.array.0::Array item",
{
name: "workflows.defaultTemplates.sample.workflows.workflow.inputExamples.example.input.array.1.name::Name",
value: "workflows.defaultTemplates.sample.workflows.workflow.inputExamples.example.input.array.1.value::Value",
},
],
});
});
it("falls back to canonical template values for custom imported JSON without keys", () => {
const custom: WorkflowsTemplateDto = {
title: "Custom",
workflows: [{ name: "Custom workflow", blocks: [{ id: "block", type: "log", description: "Custom block" }], toBlocks: [], inputExamples: [] }],
};
const workflow = custom.workflows[0];
expect(getTemplateTitle(custom, t)).toBe("Custom");
expect(getTemplateWorkflowName(custom, workflow, t)).toBe("Custom workflow");
expect(getTemplateBlockDescription(custom, workflow, workflow.blocks[0], t)).toBe("Custom block");
});
it("clones and localizes keyed templates for import persistence", () => {
const localized = localizeWorkflowTemplate(template, t);
expect(localized).not.toBe(template);
expect(localized.workflows[0]).not.toBe(template.workflows[0]);
expect(localized.title).toBe("workflows.defaultTemplates.sample.title::Sample");
expect(localized.workflows[0].name).toBe("workflows.defaultTemplates.sample.workflows.workflow.name::Workflow");
expect(localized.workflows[0].description).toBe("workflows.defaultTemplates.sample.workflows.workflow.description::Description");
expect(localized.workflows[0].blocks[0].description).toBe("workflows.defaultTemplates.sample.workflows.workflow.blocks.manual.description::Manual block");
expect(localized.workflows[0].blocks[0].input).toEqual({
message: "workflows.defaultTemplates.sample.workflows.workflow.blocks.manual.input.message::Hello",
url: "https://example.com",
});
expect(localized.workflows[0].inputExamples[0].title).toBe("workflows.defaultTemplates.sample.workflows.workflow.inputExamples.example.title::Example");
expect(localized.workflows[0].inputExamples[0].input).toEqual({
message: "workflows.defaultTemplates.sample.workflows.workflow.inputExamples.example.input.message::Hello",
nested: {
label: "Label",
},
items: ["One"],
array: [
"workflows.defaultTemplates.sample.workflows.workflow.inputExamples.example.input.array.0::Array item",
{
name: "workflows.defaultTemplates.sample.workflows.workflow.inputExamples.example.input.array.1.name::Name",
value: "workflows.defaultTemplates.sample.workflows.workflow.inputExamples.example.input.array.1.value::Value",
},
],
});
expect(template.workflows[0].name).toBe("Workflow");
});
it("leaves unkeyed custom templates unchanged apart from cloning", () => {
const custom: WorkflowsTemplateDto = {
title: "Custom",
workflows: [
{
name: "Custom workflow",
description: "Custom description",
blocks: [{ id: "block", type: "log", description: "Custom block" }],
toBlocks: [],
inputExamples: [],
},
],
};
expect(localizeWorkflowTemplate(custom, t)).toEqual(custom);
});
it("keeps built-in workflow template metadata on locale-backed keys", () => {
const en = readTranslations("en");
const de = readTranslations("de");
const keys = DefaultWorkflowTemplates.flatMap(collectTemplateMetadataKeys);
expect(keys).toContain("workflows.defaultTemplates.emails.workflows.mailjetEmail.blocks.email.input.body");
expect(keys).not.toEqual(expect.arrayContaining(["It works!", "Hello from Mailjet!", "User typed bye"]));
for (const key of keys) {
expect(key.startsWith("workflows.defaultTemplates."), key).toBe(true);
expect(getTranslation(en, key), key).toBeTypeOf("string");
expect(getTranslation(de, key), key).toBeTypeOf("string");
}
});
});
const LOCALIZABLE_TEMPLATE_INPUT_KEYS = new Set(["body", "message", "placeholder", "subject"]);
function collectTemplateMetadataKeys(template: WorkflowsTemplateDto) {
return [
template.title,
...template.workflows.flatMap((workflow) => [
workflow.name,
workflow.description,
...workflow.blocks.flatMap((block) => [
block.description,
...Object.entries(block.input ?? {})
.filter(([key, value]) => LOCALIZABLE_TEMPLATE_INPUT_KEYS.has(key) && typeof value === "string" && !value.includes("{{"))
.map(([, value]) => value as string),
]),
...workflow.inputExamples.flatMap((inputExample) => [inputExample.title, ...collectTemplateInputExampleInputKeys(template, workflow, inputExample)]),
]),
].filter((value): value is string => typeof value === "string" && value.length > 0);
}
function collectTemplateInputExampleInputKeys(template: WorkflowsTemplateDto, workflow: WorkflowsTemplateDto["workflows"][number], inputExample: WorkflowsTemplateDto["workflows"][number]["inputExamples"][number]) {
if (!template.key || !workflow.key || !inputExample.key) return [];
const prefix = `workflows.defaultTemplates.${template.key}.workflows.${workflow.key}.inputExamples.${inputExample.key}.input`;
return collectTemplateInputExampleInputKeyPaths(inputExample.input).map((path) => `${prefix}.${path}`);
}
function collectTemplateInputExampleInputKeyPaths(value: unknown, path: string[] = []): string[] {
if (typeof value === "string") return isLocalizableTemplateInputExamplePath(path) ? [path.join(".")] : [];
if (Array.isArray(value)) return value.flatMap((item, index) => collectTemplateInputExampleInputKeyPaths(item, [...path, String(index)]));
if (value && typeof value === "object") {
return Object.entries(value).flatMap(([key, entryValue]) => collectTemplateInputExampleInputKeyPaths(entryValue, [...path, key]));
}
return [];
}
function isLocalizableTemplateInputExamplePath(path: readonly string[]): boolean {
const last = path.at(-1);
if (!last) return false;
if (LOCALIZABLE_TEMPLATE_INPUT_KEYS.has(last)) return true;
if (path[0] === "array" && path.length === 3 && (last === "name" || last === "value")) return true;
return /^\d+$/u.test(last) && path.length === 2 && path[0] === "array";
}
function readTranslations(locale: "en" | "de") {
return JSON.parse(fs.readFileSync(path.join(repoRoot, `apps/web/public/locales/${locale}/translations.json`), "utf8")) as Record<string, unknown>;
}
function getTranslation(translations: Record<string, unknown>, key: string) {
return key.split(".").reduce<unknown>((value, segment) => {
if (value && typeof value === "object" && segment in value) {
return (value as Record<string, unknown>)[segment];
}
return undefined;
}, translations);
}
@@ -0,0 +1,141 @@
import type { TFunction } from "i18next";
import type { WorkflowTemplateDto, WorkflowsTemplateDto } from "../dtos/WorkflowsTemplateDto";
type TemplateBlock = WorkflowTemplateDto["blocks"][number];
type TemplateInputExample = WorkflowTemplateDto["inputExamples"][number];
const LOCALIZABLE_TEMPLATE_INPUT_KEYS = new Set(["body", "message", "placeholder", "subject"]);
function translate(t: TFunction, key: string | undefined, fallback: string | undefined) {
if (!key || !fallback) {
return fallback ?? "";
}
return t(key, { defaultValue: fallback });
}
function templatePrefix(template: WorkflowsTemplateDto) {
return template.key ? `workflows.defaultTemplates.${template.key}` : undefined;
}
function workflowPrefix(template: WorkflowsTemplateDto, workflow: WorkflowTemplateDto) {
const prefix = templatePrefix(template);
return prefix && workflow.key ? `${prefix}.workflows.${workflow.key}` : undefined;
}
export function getTemplateTitle(template: WorkflowsTemplateDto, t: TFunction) {
return translate(t, templatePrefix(template) ? `${templatePrefix(template)}.title` : undefined, template.title);
}
export function getTemplateWorkflowName(template: WorkflowsTemplateDto, workflow: WorkflowTemplateDto, t: TFunction) {
return translate(t, workflowPrefix(template, workflow) ? `${workflowPrefix(template, workflow)}.name` : undefined, workflow.name);
}
export function getTemplateWorkflowDescription(template: WorkflowsTemplateDto, workflow: WorkflowTemplateDto, t: TFunction) {
return translate(t, workflowPrefix(template, workflow) ? `${workflowPrefix(template, workflow)}.description` : undefined, workflow.description);
}
export function getTemplateBlockDescription(template: WorkflowsTemplateDto, workflow: WorkflowTemplateDto, block: TemplateBlock, t: TFunction) {
return translate(t, workflowPrefix(template, workflow) ? `${workflowPrefix(template, workflow)}.blocks.${block.id}.description` : undefined, block.description);
}
export function localizeTemplateBlockInput(
template: WorkflowsTemplateDto,
workflow: WorkflowTemplateDto,
block: TemplateBlock,
t: TFunction
): TemplateBlock["input"] {
const prefix = workflowPrefix(template, workflow);
if (!prefix || !block.input) {
return block.input;
}
let changed = false;
const input = Object.fromEntries(
Object.entries(block.input).map(([key, value]) => {
if (!LOCALIZABLE_TEMPLATE_INPUT_KEYS.has(key) || typeof value !== "string") {
return [key, value];
}
changed = true;
return [key, translate(t, `${prefix}.blocks.${block.id}.input.${key}`, value)];
})
);
return changed ? input : block.input;
}
export function getTemplateInputExampleTitle(
template: WorkflowsTemplateDto,
workflow: WorkflowTemplateDto,
inputExample: TemplateInputExample,
t: TFunction
) {
return translate(
t,
workflowPrefix(template, workflow) && inputExample.key ? `${workflowPrefix(template, workflow)}.inputExamples.${inputExample.key}.title` : undefined,
inputExample.title
);
}
export function localizeTemplateInputExampleInput(
template: WorkflowsTemplateDto,
workflow: WorkflowTemplateDto,
inputExample: TemplateInputExample,
t: TFunction
): TemplateInputExample["input"] {
const prefix = workflowPrefix(template, workflow);
if (!prefix || !inputExample.key) {
return inputExample.input;
}
return localizeTemplateInputExampleValue(inputExample.input, `${prefix}.inputExamples.${inputExample.key}.input`, t) as TemplateInputExample["input"];
}
export function localizeWorkflowTemplate(template: WorkflowsTemplateDto, t: TFunction): WorkflowsTemplateDto {
return {
...template,
title: getTemplateTitle(template, t),
workflows: template.workflows.map((workflow) => ({
...workflow,
name: getTemplateWorkflowName(template, workflow, t),
description: getTemplateWorkflowDescription(template, workflow, t),
blocks: workflow.blocks.map((block) => {
const localizedInput = localizeTemplateBlockInput(template, workflow, block, t);
return localizedInput
? {
...block,
description: getTemplateBlockDescription(template, workflow, block, t),
input: localizedInput,
}
: {
...block,
description: getTemplateBlockDescription(template, workflow, block, t),
};
}),
inputExamples: workflow.inputExamples.map((inputExample) => ({
...inputExample,
title: getTemplateInputExampleTitle(template, workflow, inputExample, t),
input: localizeTemplateInputExampleInput(template, workflow, inputExample, t),
})),
})),
};
}
function localizeTemplateInputExampleValue(value: unknown, key: string, t: TFunction, path: readonly string[] = []): unknown {
if (typeof value === "string") {
return isLocalizableTemplateInputExamplePath(path) ? translate(t, key, value) : value;
}
if (Array.isArray(value)) {
return value.map((item, index) => localizeTemplateInputExampleValue(item, `${key}.${index}`, t, [...path, String(index)]));
}
if (value && typeof value === "object") {
return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [entryKey, localizeTemplateInputExampleValue(entryValue, `${key}.${entryKey}`, t, [...path, entryKey])]));
}
return value;
}
function isLocalizableTemplateInputExamplePath(path: readonly string[]): boolean {
const last = path.at(-1);
if (!last) return false;
if (LOCALIZABLE_TEMPLATE_INPUT_KEYS.has(last)) return true;
if (path[0] === "array" && path.length === 3 && (last === "name" || last === "value")) return true;
return /^\d+$/u.test(last) && path.length === 2 && path[0] === "array";
}