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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ jobs:
- name: Install Dependencies
working-directory: ./backend
run: npm ci
- name: Generate Prisma Client
working-directory: ./backend
run: npx prisma generate
- name: Run Tests
working-directory: ./backend
run: NODE_OPTIONS="--experimental-vm-modules" npm test
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ jobs:
- name: Install Dependencies
working-directory: ./backend
run: npm ci
- name: Generate Prisma Client
working-directory: ./backend
run: npx prisma generate
- name: Run Tests
working-directory: ./backend
run: NODE_OPTIONS="--experimental-vm-modules" npm test
Expand Down
12 changes: 12 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Database connection
# Preset to match the local docker-compose defaults
DATABASE_URL="postgresql://user:password@localhost:5432/workflow_db?schema=public"

# Environment (development | production | test)
NODE_ENV="development"

# Logging configuration
LOG_LEVEL="info"

# Application Port
PORT=4000

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
-- CreateEnum
CREATE TYPE "RunStatus" AS ENUM ('SUCCESS', 'SKIPPED', 'FAILED');

-- CreateTable
CREATE TABLE "workflows" (
"id" BIGSERIAL NOT NULL,
"id" SERIAL NOT NULL,
"name" VARCHAR(255) NOT NULL,
"enabled" BOOLEAN NOT NULL DEFAULT true,
"trigger_path" VARCHAR(64) NOT NULL,
Expand All @@ -13,9 +16,9 @@ CREATE TABLE "workflows" (

-- CreateTable
CREATE TABLE "workflow_runs" (
"id" BIGSERIAL NOT NULL,
"workflow_id" BIGINT NOT NULL,
"status" VARCHAR(20) NOT NULL,
"id" SERIAL NOT NULL,
"workflow_id" INTEGER NOT NULL,
"status" "RunStatus" NOT NULL,
"start_time" TIMESTAMP(3) NOT NULL,
"end_time" TIMESTAMP(3),
"error_message" TEXT,
Expand Down
8 changes: 7 additions & 1 deletion backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ datasource db {
provider = "postgresql"
}

enum RunStatus {
SUCCESS
SKIPPED
FAILED
}

model Workflow {
id Int @id @default(autoincrement())
name String @db.VarChar(255)
Expand All @@ -23,7 +29,7 @@ model Workflow {
model WorkflowRun {
id Int @id @default(autoincrement())
workflowId Int @map("workflow_id")
status String @db.VarChar(20)
status RunStatus
startTime DateTime @map("start_time")
endTime DateTime? @map("end_time")
errorMessage String? @map("error_message")
Expand Down
62 changes: 0 additions & 62 deletions backend/scripts/validate-trigger.ts

This file was deleted.

11 changes: 5 additions & 6 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import express from "express";
import cors from "cors";
import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi";
import { z } from "zod";
import workflowRoutes from "./routes/workflow.routes";
import triggerRoutes from "./routes/trigger.routes";
import swaggerUi from "swagger-ui-express";
import { openApiDocument } from "./docs/openapi";
import { errorHandler } from "./middleware/error-handler";

extendZodWithOpenApi(z);

Expand All @@ -15,16 +20,10 @@ app.get("/health", (req, res) => {
res.json({ status: "ok", timestamp: new Date().toISOString() });
});

import workflowRoutes from "./routes/workflow.routes";
import triggerRoutes from "./routes/trigger.routes";
import swaggerUi from "swagger-ui-express";
import { openApiDocument } from "./docs/openapi";

app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(openApiDocument));
app.use("/api/workflows", workflowRoutes);
app.use("/t", triggerRoutes);

import { errorHandler } from "./middleware/error-handler";
app.use(errorHandler);

export default app;
2 changes: 0 additions & 2 deletions backend/src/lib/errors.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
export class AppError extends Error {
public readonly statusCode: number;
public readonly isOperational: boolean;

constructor(message: string, statusCode: number) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;

Error.captureStackTrace(this, this.constructor);
}
Expand Down
3 changes: 1 addition & 2 deletions backend/src/middleware/error-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ export const errorHandler: ErrorRequestHandler = (
let statusCode = err.statusCode || 500;
let message = err.message || "Internal Server Error";

// Log the error for internal tracking (could use a proper logger here)
if (statusCode === 500) {
logger.error(
{ err, req: { method: req.method, url: req.url, body: req.body } },
`[Admin API Error]: ${message}`,
`[API Error]: ${message}`,
);
}

Expand Down
6 changes: 3 additions & 3 deletions backend/src/repositories/workflow-run.repository.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { PrismaClient } from "@prisma/client";
import { PrismaClient, RunStatus } from "@prisma/client";
import prisma from "../lib/prisma";

export interface CreateRunDTO {
workflowId: number;
status: "success" | "skipped" | "failed";
status: RunStatus;
startTime: Date;
endTime?: Date;
errorMessage?: string;
failureMeta?: any;
}

export interface UpdateRunDTO {
status?: "success" | "skipped" | "failed";
status?: RunStatus;
endTime?: Date;
errorMessage?: string;
failureMeta?: any;
Expand Down
108 changes: 61 additions & 47 deletions backend/src/schemas/workflow.schema.ts
Original file line number Diff line number Diff line change
@@ -1,85 +1,99 @@
import { z } from "zod";
import {
StepType,
FilterOperator,
TransformOperator,
HttpMethod,
HttpBodyMode,
} from "../types/enums";

export const FilterStepSchema = z.object({
type: z.literal("filter"),
conditions: z.array(
z.object({
path: z.string(),
op: z.enum(["eq", "neq"]),
value: z.any(),
}),
),
});

export const LogStepSchema = z.object({
type: z.literal("log"),
message: z.string(),
type: z.literal(StepType.FILTER),
conditions: z
.array(
z.object({
path: z.string().min(1, { error: "Condition path is required" }),
op: z.enum(FilterOperator, { error: "Invalid filter operator" }),
value: z.unknown(),
}),
)
.min(1, { error: "At least one condition is required" }),
});

export const TransformStepSchema = z.object({
type: z.literal("transform"),
ops: z.array(
z.union([
z.object({
op: z.literal("default"),
path: z.string(),
value: z.any(),
}),
z.object({
op: z.literal("template"),
to: z.string(),
template: z.string(),
}),
z.object({
op: z.literal("pick"),
paths: z.array(z.string()),
}),
]),
),
type: z.literal(StepType.TRANSFORM),
ops: z
.array(
z.discriminatedUnion("op", [
z.object({
op: z.literal(TransformOperator.DEFAULT),
path: z.string().min(1, { error: "Target path is required" }),
value: z.unknown(),
}),
z.object({
op: z.literal(TransformOperator.TEMPLATE),
to: z.string().min(1, { error: "Target field 'to' is required" }),
template: z.string().min(1, { error: "Template string is required" }),
}),
z.object({
op: z.literal(TransformOperator.PICK),
paths: z
.array(z.string())
.min(1, { error: "At least one path to pick is required" }),
}),
]),
)
.min(1, { error: "At least one transformation operation is required" }),
});

export const HttpRequestStepSchema = z.object({
type: z.literal("http_request"),
method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
url: z.string().url(),
type: z.literal(StepType.HTTP_REQUEST),
method: z.enum(HttpMethod, { error: "Invalid HTTP method" }),
url: z.url({ error: "Invalid URL format" }),
headers: z.record(z.string(), z.string()).optional(),
body: z
.union([
z.object({ mode: z.literal("ctx") }),
.discriminatedUnion("mode", [
z.object({ mode: z.literal(HttpBodyMode.CTX) }),
z.object({
mode: z.literal("custom"),
value: z.record(z.string(), z.any()),
mode: z.literal(HttpBodyMode.CUSTOM),
value: z.record(z.string(), z.unknown()),
}),
])
.optional(),
timeoutMs: z.number().positive().default(2000),
retries: z.number().min(0).default(3),
retries: z
.number()
.min(0, { error: "Retries cannot be negative" })
.default(3),
});

export const StepSchema = z.union([
export const StepSchema = z.discriminatedUnion("type", [
FilterStepSchema,
LogStepSchema,
TransformStepSchema,
HttpRequestStepSchema,
]);

export const CreateWorkflowSchema = z.object({
name: z.string().min(1, "Workflow name is required"),
name: z.string().min(1, { error: "Workflow name is required" }),
enabled: z.boolean().default(true),
steps: z.array(StepSchema).min(1, "Workflow must have at least one step"),
steps: z
.array(StepSchema)
.min(1, { error: "Workflow must have at least one step" }),
});

export const UpdateWorkflowSchema = z.object({
name: z.string().min(1, "Workflow name cannot be empty").optional(),
name: z
.string()
.min(1, { error: "Workflow name cannot be empty" })
.optional(),
enabled: z.boolean().optional(),
steps: z
.array(StepSchema)
.min(1, "Workflow must have at least one step")
.min(1, { error: "Workflow must have at least one step" })
.optional(),
});

export type FilterStep = z.infer<typeof FilterStepSchema>;
export type LogStep = z.infer<typeof LogStepSchema>;
export type TransformStep = z.infer<typeof TransformStepSchema>;
export type HttpRequestStep = z.infer<typeof HttpRequestStepSchema>;
export type WorkflowStep = z.infer<typeof StepSchema>;
Expand Down
Loading