mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-14 01:50:39 +09:00
Add draft routine defaults and run-time overrides
This commit is contained in:
parent
b4a58ba8a6
commit
5d021583be
18 changed files with 592 additions and 113 deletions
2
packages/db/src/migrations/0054_draft_routines.sql
Normal file
2
packages/db/src/migrations/0054_draft_routines.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE "routines" ALTER COLUMN "project_id" DROP NOT NULL;
|
||||
ALTER TABLE "routines" ALTER COLUMN "assignee_agent_id" DROP NOT NULL;
|
||||
|
|
@ -379,6 +379,13 @@
|
|||
"when": 1775604018515,
|
||||
"tag": "0053_sharp_wild_child",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 54,
|
||||
"version": "7",
|
||||
"when": 1775750400000,
|
||||
"tag": "0054_draft_routines",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,12 +22,12 @@ export const routines = pgTable(
|
|||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
||||
projectId: uuid("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
||||
projectId: uuid("project_id").references(() => projects.id, { onDelete: "cascade" }),
|
||||
goalId: uuid("goal_id").references(() => goals.id, { onDelete: "set null" }),
|
||||
parentIssueId: uuid("parent_issue_id").references(() => issues.id, { onDelete: "set null" }),
|
||||
title: text("title").notNull(),
|
||||
description: text("description"),
|
||||
assigneeAgentId: uuid("assignee_agent_id").notNull().references(() => agents.id),
|
||||
assigneeAgentId: uuid("assignee_agent_id").references(() => agents.id),
|
||||
priority: text("priority").notNull().default("medium"),
|
||||
status: text("status").notNull().default("active"),
|
||||
concurrencyPolicy: text("concurrency_policy").notNull().default("coalesce_if_active"),
|
||||
|
|
|
|||
|
|
@ -637,8 +637,11 @@ export {
|
|||
} from "./project-mentions.js";
|
||||
|
||||
export {
|
||||
BUILTIN_ROUTINE_VARIABLE_NAMES,
|
||||
extractRoutineVariableNames,
|
||||
getBuiltinRoutineVariableValues,
|
||||
interpolateRoutineTemplate,
|
||||
isBuiltinRoutineVariable,
|
||||
isValidRoutineVariableName,
|
||||
stringifyRoutineVariableValue,
|
||||
syncRoutineVariablesWithTemplate,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
BUILTIN_ROUTINE_VARIABLE_NAMES,
|
||||
extractRoutineVariableNames,
|
||||
getBuiltinRoutineVariableValues,
|
||||
interpolateRoutineTemplate,
|
||||
isBuiltinRoutineVariable,
|
||||
syncRoutineVariablesWithTemplate,
|
||||
} from "./routine-variables.js";
|
||||
|
||||
|
|
@ -40,4 +43,34 @@ describe("routine variable helpers", () => {
|
|||
}),
|
||||
).toBe("Review paperclip for high");
|
||||
});
|
||||
|
||||
it("identifies built-in variable names", () => {
|
||||
expect(isBuiltinRoutineVariable("date")).toBe(true);
|
||||
expect(isBuiltinRoutineVariable("repo")).toBe(false);
|
||||
expect(BUILTIN_ROUTINE_VARIABLE_NAMES.has("date")).toBe(true);
|
||||
});
|
||||
|
||||
it("getBuiltinRoutineVariableValues returns date in YYYY-MM-DD format", () => {
|
||||
const values = getBuiltinRoutineVariableValues();
|
||||
expect(values.date).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
||||
expect(values.date).toBe(new Date().toISOString().slice(0, 10));
|
||||
});
|
||||
|
||||
it("excludes built-in variables from syncRoutineVariablesWithTemplate", () => {
|
||||
const result = syncRoutineVariablesWithTemplate(
|
||||
"Daily report for {{date}} — {{repo}}",
|
||||
[],
|
||||
);
|
||||
expect(result).toEqual([
|
||||
{ name: "repo", label: null, type: "text", defaultValue: null, required: true, options: [] },
|
||||
]);
|
||||
});
|
||||
|
||||
it("interpolates built-in date variable alongside user variables", () => {
|
||||
const builtins = getBuiltinRoutineVariableValues();
|
||||
const allVars = { ...builtins, repo: "paperclip" };
|
||||
expect(
|
||||
interpolateRoutineTemplate("Report for {{date}} on {{repo}}", allVars),
|
||||
).toBe(`Report for ${builtins.date} on paperclip`);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,26 @@ import type { RoutineVariable } from "./types/routine.js";
|
|||
const ROUTINE_VARIABLE_MATCHER = /\{\{\s*([A-Za-z][A-Za-z0-9_]*)\s*\}\}/g;
|
||||
type RoutineTemplateInput = string | null | undefined | Array<string | null | undefined>;
|
||||
|
||||
/**
|
||||
* Built-in variable names that are automatically available in routine templates
|
||||
* without needing to be defined in the routine's variables list.
|
||||
*/
|
||||
export const BUILTIN_ROUTINE_VARIABLE_NAMES = new Set(["date"]);
|
||||
|
||||
export function isBuiltinRoutineVariable(name: string): boolean {
|
||||
return BUILTIN_ROUTINE_VARIABLE_NAMES.has(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current values for all built-in routine variables.
|
||||
* `date` expands to the current date in YYYY-MM-DD format (UTC).
|
||||
*/
|
||||
export function getBuiltinRoutineVariableValues(): Record<string, string> {
|
||||
return {
|
||||
date: new Date().toISOString().slice(0, 10),
|
||||
};
|
||||
}
|
||||
|
||||
export function isValidRoutineVariableName(name: string): boolean {
|
||||
return /^[A-Za-z][A-Za-z0-9_]*$/.test(name);
|
||||
}
|
||||
|
|
@ -40,7 +60,7 @@ export function syncRoutineVariablesWithTemplate(
|
|||
template: RoutineTemplateInput,
|
||||
existing: RoutineVariable[] | null | undefined,
|
||||
): RoutineVariable[] {
|
||||
const names = extractRoutineVariableNames(template);
|
||||
const names = extractRoutineVariableNames(template).filter((name) => !isBuiltinRoutineVariable(name));
|
||||
const existingByName = new Map((existing ?? []).map((variable) => [variable.name, variable]));
|
||||
return names.map((name) => existingByName.get(name) ?? defaultRoutineVariable(name));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,12 +39,12 @@ export interface RoutineVariable {
|
|||
export interface Routine {
|
||||
id: string;
|
||||
companyId: string;
|
||||
projectId: string;
|
||||
projectId: string | null;
|
||||
goalId: string | null;
|
||||
parentIssueId: string | null;
|
||||
title: string;
|
||||
description: string | null;
|
||||
assigneeAgentId: string;
|
||||
assigneeAgentId: string | null;
|
||||
priority: string;
|
||||
status: string;
|
||||
concurrencyPolicy: string;
|
||||
|
|
|
|||
|
|
@ -48,12 +48,12 @@ export const routineVariableSchema = z.object({
|
|||
});
|
||||
|
||||
export const createRoutineSchema = z.object({
|
||||
projectId: z.string().uuid(),
|
||||
projectId: z.string().uuid().optional().nullable(),
|
||||
goalId: z.string().uuid().optional().nullable(),
|
||||
parentIssueId: z.string().uuid().optional().nullable(),
|
||||
title: z.string().trim().min(1).max(200),
|
||||
description: z.string().optional().nullable(),
|
||||
assigneeAgentId: z.string().uuid(),
|
||||
assigneeAgentId: z.string().uuid().optional().nullable(),
|
||||
priority: z.enum(ISSUE_PRIORITIES).optional().default("medium"),
|
||||
status: z.enum(ROUTINE_STATUSES).optional().default("active"),
|
||||
concurrencyPolicy: z.enum(ROUTINE_CONCURRENCY_POLICIES).optional().default("coalesce_if_active"),
|
||||
|
|
@ -104,6 +104,8 @@ export const runRoutineSchema = z.object({
|
|||
triggerId: z.string().uuid().optional().nullable(),
|
||||
payload: z.record(z.unknown()).optional().nullable(),
|
||||
variables: z.record(routineVariableValueSchema).optional().nullable(),
|
||||
projectId: z.string().uuid().optional().nullable(),
|
||||
assigneeAgentId: z.string().uuid().optional().nullable(),
|
||||
idempotencyKey: z.string().trim().max(255).optional().nullable(),
|
||||
source: z.enum(["manual", "api"]).optional().default("manual"),
|
||||
executionWorkspaceId: z.string().uuid().optional().nullable(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue