Skip to content

Commit 5ac8c33

Browse files
authored
Validate Dag and task IDs in the ts-sdk task registry (#69400)
* Validate Dag and task IDs in the ts-sdk task registry * Validate Dag and task ID length in the ts-sdk task registry
1 parent da4643c commit 5ac8c33

2 files changed

Lines changed: 56 additions & 8 deletions

File tree

ts-sdk/src/sdk/registry.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,21 @@
1919

2020
import type { TaskHandler } from "./task.js";
2121

22+
// Mirrors the Python task-SDK KEY_REGEX and validate_key in airflow.sdk.definitions._internal.node.
23+
const KEY_REGEX = /^[\p{L}\p{N}_.-]+$/u;
24+
const MAX_KEY_LENGTH = 250;
25+
26+
function validateKey(name: string, value: string): void {
27+
if (typeof value !== "string" || !KEY_REGEX.test(value)) {
28+
throw new Error(
29+
`${name} must be made of alphanumeric characters, dashes, dots, and underscores`,
30+
);
31+
}
32+
if (value.length > MAX_KEY_LENGTH) {
33+
throw new Error(`${name} must be less than ${MAX_KEY_LENGTH} characters, not ${value.length}`);
34+
}
35+
}
36+
2237
/** Identifies the Airflow task handled by a TypeScript function. */
2338
export interface TaskRegistration {
2439
/** Identifier of the Dag containing this task. */
@@ -39,12 +54,8 @@ export class TaskRegistry {
3954
*/
4055
register<TReturn = unknown>(registration: TaskRegistration, handler: TaskHandler<TReturn>): void {
4156
const { dagId, taskId } = registration;
42-
if (!dagId || typeof dagId !== "string") {
43-
throw new Error("dagId must be a non-empty string");
44-
}
45-
if (!taskId || typeof taskId !== "string") {
46-
throw new Error("taskId must be a non-empty string");
47-
}
57+
validateKey("dagId", dagId);
58+
validateKey("taskId", taskId);
4859
if (typeof handler !== "function") {
4960
throw new Error(`handler for Dag "${dagId}" task "${taskId}" must be a function`);
5061
}

ts-sdk/tests/sdk/registry.test.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,51 @@ describe("registry", () => {
7373
const registry = new TaskRegistry();
7474
expect(() =>
7575
registry.register({ dagId: "", taskId: "my_task" }, async () => undefined),
76-
).toThrowError(/dagId must be a non-empty string/);
76+
).toThrowError(/dagId must be made of alphanumeric/);
7777
});
7878

7979
it("rejects an empty taskId", () => {
8080
const registry = new TaskRegistry();
8181
expect(() =>
8282
registry.register({ dagId: "example_dag", taskId: "" }, async () => undefined),
83-
).toThrowError(/taskId must be a non-empty string/);
83+
).toThrowError(/taskId must be made of alphanumeric/);
84+
});
85+
86+
it.each([" ", "\t", "my dag", "a/b", "task@1"])(
87+
"rejects a dagId with characters no Python dag_id allows: %j",
88+
(dagId) => {
89+
const registry = new TaskRegistry();
90+
expect(() =>
91+
registry.register({ dagId, taskId: "my_task" }, async () => undefined),
92+
).toThrowError(/dagId must be made of alphanumeric/);
93+
},
94+
);
95+
96+
it.each([" ", "\t", "my task", "a/b", "task@1"])(
97+
"rejects a taskId with characters no Python task_id allows: %j",
98+
(taskId) => {
99+
const registry = new TaskRegistry();
100+
expect(() =>
101+
registry.register({ dagId: "example_dag", taskId }, async () => undefined),
102+
).toThrowError(/taskId must be made of alphanumeric/);
103+
},
104+
);
105+
106+
it.each([
107+
["dagId", { dagId: "d".repeat(251), taskId: "my_task" }],
108+
["taskId", { dagId: "example_dag", taskId: "t".repeat(251) }],
109+
])("rejects a %s longer than 250 characters", (name, registration) => {
110+
const registry = new TaskRegistry();
111+
expect(() => registry.register(registration, async () => undefined)).toThrowError(
112+
new RegExp(`${name} must be less than 250 characters, not 251`),
113+
);
114+
});
115+
116+
it("accepts a Unicode dagId that Python's word-character rule allows", () => {
117+
const registry = new TaskRegistry();
118+
const handler = async () => undefined;
119+
registry.register({ dagId: "café_dag", taskId: "任務" }, handler);
120+
expect(registry.get("café_dag", "任務")).toBe(handler);
84121
});
85122

86123
it("rejects non-function handlers", () => {

0 commit comments

Comments
 (0)