* feat(yaml-validator)!: unify flow, schedule, and trigger validation - replace FlowValidator with WindmillYamlValidator.validate(doc, target) - generate schedule/trigger schemas from backend OpenAPI and OpenFlow refs - add schedule/trigger/filename-target tests and update AI agent fixtures - bump windmill-yaml-validator to 2.0.0 BREAKING CHANGE: FlowValidator and validateFlow() are replaced by WindmillYamlValidator.validate(doc, target). * add lint command * add deno-compat script and docs for local yaml-validator testing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: make nullable fields pass yaml validation Add nullable: true to static_asset_config and authentication_resource_path in HttpTrigger schema. Post-process generated JSON schemas to add null to enums with nullable: true (AJV doesn't handle OpenAPI 3.0 nullable + enum). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add nullable to all Option<T> fields in trigger and schedule OpenAPI schemas Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(frontend): handle nullable fields from updated OpenAPI types Add ?? undefined coalescing at assignment sites where generated types now include | null from the OpenAPI nullable additions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(lint): show allowed values in enum validation errors Instead of "must be equal to one of the allowed values", now shows "must be one of: 'r', 'w', 'rw'" for enum validation failures. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add nullable to Edit/New trigger and schedule OpenAPI schemas Ensures create/update request body types accept null for the same fields that GET response types return as nullable, enabling clean round-tripping without type mismatches. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * use published package * publish * refactor(lint): remove unused --includes/--excludes/--extra-includes CLI options These options were defined but never wired to the file filtering logic. The lint command still respects includes/excludes from wmill.yaml via mergeConfigWithConfigFile. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(lint): handle additionalProperties errors and expand test coverage Add formatting for AJV additionalProperties keyword to show the unknown property name. Add unit tests for all formatValidationError branches and integration tests for --json report shape, --fail-on-warn with mixed files, non-existent directory, and enum error output. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add realistic validator tests for schedules, triggers, and edge cases Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add email trigger validation support Add email trigger schema generation, validation, and linting. Email triggers are no longer skipped with a warning — they are validated like all other trigger types. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore(cli): bump windmill-yaml-validator to 1.1.1 (email trigger support) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * publish * rm * fix: address PR review feedback for lint command - Add email to trigger kinds test loop instead of separate test - Add email to ValidationTarget docs in README - Type formatYamlDiagnostics param directly instead of unsafe cast - Destructure json option before mergeConfigWithConfigFile for clarity Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(cli): add --lint option to sync push command Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
142 lines
4.1 KiB
TypeScript
142 lines
4.1 KiB
TypeScript
import Ajv, { AnySchema, ErrorObject, ValidateFunction } from "ajv";
|
|
import { parseWithPointers, YamlParserResult } from "@stoplight/yaml";
|
|
import openFlowSchema from "../gen/openflow.json";
|
|
import scheduleSchema from "../gen/schedule.json";
|
|
import gcpTriggerSchema from "../gen/triggers/gcp.json";
|
|
import httpTriggerSchema from "../gen/triggers/http.json";
|
|
import kafkaTriggerSchema from "../gen/triggers/kafka.json";
|
|
import mqttTriggerSchema from "../gen/triggers/mqtt.json";
|
|
import natsTriggerSchema from "../gen/triggers/nats.json";
|
|
import postgresTriggerSchema from "../gen/triggers/postgres.json";
|
|
import sqsTriggerSchema from "../gen/triggers/sqs.json";
|
|
import websocketTriggerSchema from "../gen/triggers/websocket.json";
|
|
import emailTriggerSchema from "../gen/triggers/email.json";
|
|
|
|
export const SUPPORTED_TRIGGER_KINDS = [
|
|
"http",
|
|
"websocket",
|
|
"kafka",
|
|
"nats",
|
|
"postgres",
|
|
"mqtt",
|
|
"sqs",
|
|
"gcp",
|
|
"email",
|
|
] as const;
|
|
|
|
export type TriggerKind = (typeof SUPPORTED_TRIGGER_KINDS)[number];
|
|
|
|
export type ValidationTarget =
|
|
| { type: "flow" }
|
|
| { type: "schedule" }
|
|
| { type: "trigger"; triggerKind: TriggerKind };
|
|
|
|
const TRIGGER_SCHEMAS: Record<TriggerKind, AnySchema> = {
|
|
http: httpTriggerSchema as AnySchema,
|
|
websocket: websocketTriggerSchema as AnySchema,
|
|
kafka: kafkaTriggerSchema as AnySchema,
|
|
nats: natsTriggerSchema as AnySchema,
|
|
postgres: postgresTriggerSchema as AnySchema,
|
|
mqtt: mqttTriggerSchema as AnySchema,
|
|
sqs: sqsTriggerSchema as AnySchema,
|
|
gcp: gcpTriggerSchema as AnySchema,
|
|
email: emailTriggerSchema as AnySchema,
|
|
};
|
|
|
|
/**
|
|
* Infers validation target from file name conventions used by Windmill sync.
|
|
*/
|
|
export function getValidationTargetFromFilename(
|
|
filePath: string
|
|
): ValidationTarget | null {
|
|
const path = filePath.toLowerCase();
|
|
|
|
if (/[/\\]flow\.ya?ml$/.test(path) || /^flow\.ya?ml$/.test(path)) {
|
|
return { type: "flow" };
|
|
}
|
|
|
|
if (/\.schedule\.ya?ml$/.test(path)) {
|
|
return { type: "schedule" };
|
|
}
|
|
|
|
const triggerMatch = path.match(
|
|
/\.(http|websocket|kafka|nats|postgres|mqtt|sqs|gcp|email)_trigger\.ya?ml$/
|
|
);
|
|
if (triggerMatch) {
|
|
return {
|
|
type: "trigger",
|
|
triggerKind: triggerMatch[1] as TriggerKind,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Unified YAML validator for Windmill flow, schedule, and trigger files.
|
|
*/
|
|
export class WindmillYamlValidator {
|
|
private readonly validateFlow: ValidateFunction;
|
|
private readonly validateSchedule: ValidateFunction;
|
|
private readonly validateTrigger: Record<TriggerKind, ValidateFunction>;
|
|
|
|
constructor() {
|
|
const ajv = new Ajv({
|
|
strict: false,
|
|
allErrors: true,
|
|
discriminator: true,
|
|
validateFormats: false,
|
|
});
|
|
|
|
for (const [name, schema] of Object.entries(openFlowSchema.components.schemas)) {
|
|
ajv.addSchema(schema as AnySchema, `#/components/schemas/${name}`);
|
|
}
|
|
|
|
this.validateFlow = ajv.getSchema("#/components/schemas/OpenFlow")!;
|
|
this.validateSchedule = ajv.compile(scheduleSchema as AnySchema);
|
|
|
|
this.validateTrigger = Object.fromEntries(
|
|
SUPPORTED_TRIGGER_KINDS.map((kind) => [kind, ajv.compile(TRIGGER_SCHEMAS[kind])])
|
|
) as Record<TriggerKind, ValidateFunction>;
|
|
}
|
|
|
|
/**
|
|
* Validates a Windmill YAML document based on the selected target.
|
|
* @param doc - The YAML document as string
|
|
* @param target - Which Windmill schema to validate against
|
|
*/
|
|
validate(
|
|
doc: string,
|
|
target: ValidationTarget
|
|
): { parsed: YamlParserResult<unknown>; errors: ErrorObject[] } {
|
|
if (typeof doc !== "string") {
|
|
throw new Error("Document must be a string");
|
|
}
|
|
|
|
const parsed = parseWithPointers(doc);
|
|
const { data } = parsed;
|
|
|
|
let validator: ValidateFunction;
|
|
if (target.type === "flow") {
|
|
validator = this.validateFlow;
|
|
} else if (target.type === "schedule") {
|
|
validator = this.validateSchedule;
|
|
} else {
|
|
validator = this.validateTrigger[target.triggerKind];
|
|
if (!validator) {
|
|
throw new Error(`Unsupported trigger kind: ${target.triggerKind}`);
|
|
}
|
|
}
|
|
|
|
const ok = validator(data);
|
|
if (ok) {
|
|
return { parsed, errors: [] };
|
|
}
|
|
|
|
return {
|
|
parsed,
|
|
errors: validator.errors || [],
|
|
};
|
|
}
|
|
}
|