feat: project schema approval fields

This commit is contained in:
lda
2026-07-09 05:40:20 +07:00 Verified
parent 216f13e471
commit f5a81106dd
2 changed files with 177 additions and 0 deletions
@@ -0,0 +1,87 @@
import { describe, expect, it } from "vitest";
import { buildSchemaApprovalModel } from "./schema-approval-model.js";
describe("buildSchemaApprovalModel", () => {
it("projects explicit object schema properties into approval fields", () => {
const model = buildSchemaApprovalModel({
schema: {
type: "object",
required: ["selected_issue_ids"],
properties: {
selected_issue_ids: {
type: "array",
description: "Issue ids to create",
items: { type: "string" },
},
comment: { type: "string" },
approved: { type: "boolean" },
},
},
payload: {
selected_issue_ids: ["risk-1"],
comment: "Create the selected issue.",
approved: true,
},
outcomes: ["submitted", "cancelled"],
});
expect(model.hasExplicitFields).toBe(true);
expect(model.outcomes).toEqual(["submitted", "cancelled"]);
expect(model.fields).toEqual([
{
name: "selected_issue_ids",
label: "selected issue ids",
kind: "array",
required: true,
description: "Issue ids to create",
valuePreview: "[\"risk-1\"]",
},
{
name: "comment",
label: "comment",
kind: "string",
required: false,
description: null,
valuePreview: "Create the selected issue.",
},
{
name: "approved",
label: "approved",
kind: "boolean",
required: false,
description: null,
valuePreview: "true",
},
]);
});
it("uses payload preview when schema is a loose object without properties", () => {
const model = buildSchemaApprovalModel({
schema: { type: "object" },
payload: {
selected_issue_ids: ["risk-1"],
comment: "Create the selected issue.",
},
outcomes: ["submitted", "cancelled"],
});
expect(model.hasExplicitFields).toBe(false);
expect(model.fields).toEqual([]);
expect(model.payloadPreview).toEqual([
{ key: "selected_issue_ids", value: "[\"risk-1\"]" },
{ key: "comment", value: "Create the selected issue." },
]);
});
it("handles non-object schema without throwing", () => {
const model = buildSchemaApprovalModel({
schema: true,
payload: null,
outcomes: [],
});
expect(model.hasExplicitFields).toBe(false);
expect(model.fields).toEqual([]);
expect(model.payloadPreview).toEqual([]);
});
});
@@ -0,0 +1,90 @@
export type SchemaApprovalFieldKind = "string" | "number" | "boolean" | "array" | "object" | "unknown";
export type SchemaApprovalField = {
readonly name: string;
readonly label: string;
readonly kind: SchemaApprovalFieldKind;
readonly required: boolean;
readonly description: string | null;
readonly valuePreview: string | null;
};
export type SchemaApprovalModel = {
readonly hasExplicitFields: boolean;
readonly fields: ReadonlyArray<SchemaApprovalField>;
readonly payloadPreview: ReadonlyArray<{ readonly key: string; readonly value: string }>;
readonly outcomes: ReadonlyArray<string>;
};
export type BuildSchemaApprovalModelInput = {
readonly schema: unknown;
readonly payload: unknown;
readonly outcomes: ReadonlyArray<string>;
};
type JsonObject = Record<string, unknown>;
const isObject = (value: unknown): value is JsonObject =>
typeof value === "object" && value !== null && !Array.isArray(value);
const stringArray = (value: unknown): ReadonlyArray<string> =>
Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === "string") : [];
const formatValue = (value: unknown): string | null => {
if (value === undefined) return null;
if (typeof value === "string") return value;
if (typeof value === "number" || typeof value === "boolean" || value === null) return String(value);
return JSON.stringify(value);
};
const fieldKind = (propertySchema: unknown): SchemaApprovalFieldKind => {
if (!isObject(propertySchema)) return "unknown";
const type = propertySchema.type;
if (type === "integer") return "number";
if (type === "string" || type === "number" || type === "boolean" || type === "array" || type === "object") {
return type;
}
return "unknown";
};
const labelFor = (name: string): string => name.replaceAll("_", " ");
const payloadEntries = (payload: unknown): ReadonlyArray<{ readonly key: string; readonly value: string }> => {
if (!isObject(payload)) return [];
return Object.entries(payload).map(([key, value]) => ({
key,
value: formatValue(value) ?? "undefined",
}));
};
export const buildSchemaApprovalModel = ({
schema,
payload,
outcomes,
}: BuildSchemaApprovalModelInput): SchemaApprovalModel => {
const required = isObject(schema) ? new Set(stringArray(schema.required)) : new Set<string>();
const properties = isObject(schema) && isObject(schema.properties) ? schema.properties : null;
const payloadObject = isObject(payload) ? payload : {};
// This is projection, not validation. For loose schemas the product should
// still show the recorded resume payload instead of inventing absent fields.
const fields = properties
? Object.entries(properties).map(([name, propertySchema]): SchemaApprovalField => ({
name,
label: labelFor(name),
kind: fieldKind(propertySchema),
required: required.has(name),
description: isObject(propertySchema) && typeof propertySchema.description === "string"
? propertySchema.description
: null,
valuePreview: formatValue(payloadObject[name]),
}))
: [];
return {
hasExplicitFields: fields.length > 0,
fields,
payloadPreview: payloadEntries(payload),
outcomes,
};
};