feat(schema): add OperationNodeAttrs and CallNodeAttrs TypeBox schemas
This commit is contained in:
@@ -9,3 +9,5 @@ export {
|
|||||||
type EdgeType,
|
type EdgeType,
|
||||||
Nullable,
|
Nullable,
|
||||||
} from "./enums.js";
|
} from "./enums.js";
|
||||||
|
|
||||||
|
export * from "./node.js";
|
||||||
|
|||||||
@@ -1 +1,40 @@
|
|||||||
export {};
|
import { Type, type Static } from "@alkdev/typebox";
|
||||||
|
import { OperationTypeEnum, CallStatusEnum } from "./enums.js";
|
||||||
|
|
||||||
|
export const OperationNodeAttrs = Type.Object({
|
||||||
|
name: Type.String(),
|
||||||
|
namespace: Type.String(),
|
||||||
|
version: Type.String(),
|
||||||
|
type: OperationTypeEnum,
|
||||||
|
inputSchema: Type.Unknown(),
|
||||||
|
outputSchema: Type.Unknown(),
|
||||||
|
description: Type.Optional(Type.String()),
|
||||||
|
tags: Type.Optional(Type.Array(Type.String())),
|
||||||
|
});
|
||||||
|
export type OperationNodeAttrs = Static<typeof OperationNodeAttrs>;
|
||||||
|
|
||||||
|
export const CallNodeAttrs = Type.Object({
|
||||||
|
requestId: Type.String(),
|
||||||
|
operationId: Type.String(),
|
||||||
|
status: CallStatusEnum,
|
||||||
|
parentRequestId: Type.Optional(Type.String()),
|
||||||
|
input: Type.Unknown(),
|
||||||
|
output: Type.Optional(Type.Unknown()),
|
||||||
|
error: Type.Optional(
|
||||||
|
Type.Object({
|
||||||
|
code: Type.String(),
|
||||||
|
message: Type.String(),
|
||||||
|
details: Type.Optional(Type.Unknown()),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
identity: Type.Optional(
|
||||||
|
Type.Object({
|
||||||
|
id: Type.String(),
|
||||||
|
scopes: Type.Array(Type.String()),
|
||||||
|
resources: Type.Optional(Type.Record(Type.String(), Type.Array(Type.String()))),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
startedAt: Type.Optional(Type.String()),
|
||||||
|
completedAt: Type.Optional(Type.String()),
|
||||||
|
});
|
||||||
|
export type CallNodeAttrs = Static<typeof CallNodeAttrs>;
|
||||||
131
test/schema/node.test.ts
Normal file
131
test/schema/node.test.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { Value } from "@alkdev/typebox/value";
|
||||||
|
import {
|
||||||
|
OperationNodeAttrs,
|
||||||
|
type OperationNodeAttrs as OperationNodeAttrsType,
|
||||||
|
CallNodeAttrs,
|
||||||
|
type CallNodeAttrs as CallNodeAttrsType,
|
||||||
|
} from "../../src/schema/node";
|
||||||
|
|
||||||
|
describe("OperationNodeAttrs", () => {
|
||||||
|
const valid: OperationNodeAttrsType = {
|
||||||
|
name: "classify",
|
||||||
|
namespace: "task",
|
||||||
|
version: "1.0.0",
|
||||||
|
type: "query",
|
||||||
|
inputSchema: { type: "object" },
|
||||||
|
outputSchema: { type: "object" },
|
||||||
|
};
|
||||||
|
|
||||||
|
it("accepts valid attributes without optional fields", () => {
|
||||||
|
expect(Value.Check(OperationNodeAttrs, valid)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts valid attributes with all optional fields", () => {
|
||||||
|
const withOptional: OperationNodeAttrsType = {
|
||||||
|
...valid,
|
||||||
|
description: "Classifies input",
|
||||||
|
tags: ["ml", "nlp"],
|
||||||
|
};
|
||||||
|
expect(Value.Check(OperationNodeAttrs, withOptional)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts all operation types", () => {
|
||||||
|
for (const type of ["query", "mutation", "subscription"] as const) {
|
||||||
|
expect(Value.Check(OperationNodeAttrs, { ...valid, type })).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects missing required fields", () => {
|
||||||
|
expect(Value.Check(OperationNodeAttrs, {})).toBe(false);
|
||||||
|
expect(Value.Check(OperationNodeAttrs, { name: "classify" })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects invalid operation type", () => {
|
||||||
|
expect(
|
||||||
|
Value.Check(OperationNodeAttrs, { ...valid, type: "invalid" }),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects wrong types for optional tags", () => {
|
||||||
|
expect(Value.Check(OperationNodeAttrs, { ...valid, tags: [1, 2] })).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("CallNodeAttrs", () => {
|
||||||
|
const valid: CallNodeAttrsType = {
|
||||||
|
requestId: "req_abc123",
|
||||||
|
operationId: "task.classify",
|
||||||
|
status: "pending",
|
||||||
|
input: { text: "hello" },
|
||||||
|
};
|
||||||
|
|
||||||
|
it("accepts valid attributes without optional fields", () => {
|
||||||
|
expect(Value.Check(CallNodeAttrs, valid)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts valid attributes with all optional fields", () => {
|
||||||
|
const withOptional: CallNodeAttrsType = {
|
||||||
|
...valid,
|
||||||
|
parentRequestId: "req_parent",
|
||||||
|
output: { label: "greeting" },
|
||||||
|
error: {
|
||||||
|
code: "INTERNAL",
|
||||||
|
message: "Something went wrong",
|
||||||
|
details: { stack: "..." },
|
||||||
|
},
|
||||||
|
identity: {
|
||||||
|
id: "user_1",
|
||||||
|
scopes: ["read", "write"],
|
||||||
|
resources: { org: ["org_1"] },
|
||||||
|
},
|
||||||
|
startedAt: "2026-05-21T10:00:00Z",
|
||||||
|
completedAt: "2026-05-21T10:00:01Z",
|
||||||
|
};
|
||||||
|
expect(Value.Check(CallNodeAttrs, withOptional)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts all call statuses", () => {
|
||||||
|
for (const status of ["pending", "running", "completed", "failed", "aborted"] as const) {
|
||||||
|
expect(Value.Check(CallNodeAttrs, { ...valid, status })).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts error without optional details", () => {
|
||||||
|
const withError: CallNodeAttrsType = {
|
||||||
|
...valid,
|
||||||
|
error: { code: "NOT_FOUND", message: "Operation not found" },
|
||||||
|
};
|
||||||
|
expect(Value.Check(CallNodeAttrs, withError)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts identity without optional resources", () => {
|
||||||
|
const withIdentity: CallNodeAttrsType = {
|
||||||
|
...valid,
|
||||||
|
identity: { id: "user_1", scopes: ["read"] },
|
||||||
|
};
|
||||||
|
expect(Value.Check(CallNodeAttrs, withIdentity)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects missing required fields", () => {
|
||||||
|
expect(Value.Check(CallNodeAttrs, {})).toBe(false);
|
||||||
|
expect(Value.Check(CallNodeAttrs, { requestId: "req_1" })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects invalid call status", () => {
|
||||||
|
expect(
|
||||||
|
Value.Check(CallNodeAttrs, { ...valid, status: "idle" }),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects error with missing required fields", () => {
|
||||||
|
expect(
|
||||||
|
Value.Check(CallNodeAttrs, { ...valid, error: { code: "X" } }),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
Value.Check(CallNodeAttrs, { ...valid, error: { message: "X" } }),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user