Function: validatePublishable()
function validatePublishable(input: {
attachedRuleIds?: Set<string>;
entryStepKey: string | null;
rules: {
createdAt: Date;
createdBy: string | null;
currentVersionId: string | null;
deletedAt: Date | null;
deletedBy: string | null;
description: string | null;
domain: string;
id: string;
identifier: string;
module: string | null;
name: string;
organizationId: string;
rateTierId: string | null;
scheduleConditionId: string | null;
status: RuleStatus;
statusUpdatedAt: Date;
statusUpdatedBy: string | null;
updatedAt: Date;
updatedBy: string | null;
}[];
steps: {
description: string | null;
fields: {
additionalInfo?: {
body?: string;
title?: string;
};
adoptionStatement?: string;
allowedFonts?: string[];
asset?: {
alt?: string;
fileId?: string;
filename?: string;
mimeType?: string;
url?: string;
};
dataSource?: string;
defaultValue?: | string
| {
from: string;
to: string | null;
};
description?: string;
fields?: {
additionalInfo?: {
body?: ... | ...;
title?: ... | ...;
};
adoptionStatement?: string;
allowedFonts?: string[];
asset?: {
alt?: ... | ...;
fileId?: ... | ...;
filename?: ... | ...;
mimeType?: ... | ...;
url?: ... | ...;
};
dataSource?: string;
defaultValue?: | string
| {
from: string;
to: ... | ...;
};
description?: string;
format?: string;
label: string;
maxDate?: string;
maxFiles?: number;
maxLength?: number;
minDate?: string;
name: string;
options?: (... | ...)[];
placeholder?: string;
prefillFrom?: string;
required: boolean;
showRange?: boolean;
showTime?: boolean;
signatureMode?: "typed" | "drawn";
timezone?: string;
type: | "number"
| "date"
| "file"
| "email"
| "url"
| "select"
| "text"
| "checkbox"
| "textarea"
| "phone"
| "radio-group"
| "multiselect"
| "field-array"
| "image"
| "file-download"
| "section-header"
| "section-unset"
| "signature"
| "signature-adopted";
}[];
format?: string;
label: string;
maxDate?: string;
maxFiles?: number;
maxLength?: number;
maxRows?: number;
minDate?: string;
minRows?: number;
name: string;
options?: (
| string
| {
description?: string;
value: string;
})[];
placeholder?: string;
prefillFrom?: string;
required: boolean;
showRange?: boolean;
showTime?: boolean;
signatureMode?: "typed" | "drawn";
timezone?: string;
type: | "number"
| "date"
| "file"
| "email"
| "url"
| "select"
| "text"
| "checkbox"
| "textarea"
| "phone"
| "radio-group"
| "multiselect"
| "field-array"
| "image"
| "file-download"
| "section-header"
| "section-unset"
| "signature"
| "signature-adopted";
}[];
formSchemaVersionId: string;
id: string;
key: string;
label: string | null;
}[];
transitions: {
fromStepId: string;
id: string;
label: string | null;
priority: number;
ruleId: string | null;
ruleVersionId: string | null;
toStepId: string;
}[];
}): PublishValidationResult;Defined in: server/forms/publish-validators.ts:43
Determine if a form is publishable, where "publishable" means:
- The entry step exists (for starting)
- A final step exists (for submitting)
- There are no cycles
- There are no unreachable/orphaned steps
- Every transition's
ruleIdresolves to a known rule - Every transition's
ruleIdis attached to the schema's rule pool - Outgoing transitions from a step have unique priorities
- Field names have stable types across the version's steps
Pure validator that enforces the publish-time invariants from the spec. Operates entirely on in-memory data — no DB access, no async — so it's trivially unit-testable.
Returned referencedRuleIds is always populated (even on failure)
so the publish flow can do a two-pass walk: first to discover
referenced ids, then to validate after the rules are loaded.
attachedRuleIds is the set of rule ids attached to the schema via
FormSchemaRules. Pass undefined to skip the attachment check
(callers in tests or older codepaths). When provided, every
referenced ruleId must be in the set.
Parameters
| Parameter | Type |
|---|---|
input | { attachedRuleIds?: Set<string>; entryStepKey: string | null; rules: { createdAt: Date; createdBy: string | null; currentVersionId: string | null; deletedAt: Date | null; deletedBy: string | null; description: string | null; domain: string; id: string; identifier: string; module: string | null; name: string; organizationId: string; rateTierId: string | null; scheduleConditionId: string | null; status: RuleStatus; statusUpdatedAt: Date; statusUpdatedBy: string | null; updatedAt: Date; updatedBy: string | null; }[]; steps: { description: string | null; fields: { additionalInfo?: { body?: string; title?: string; }; adoptionStatement?: string; allowedFonts?: string[]; asset?: { alt?: string; fileId?: string; filename?: string; mimeType?: string; url?: string; }; dataSource?: string; defaultValue?: | string | { from: string; to: string | null; }; description?: string; fields?: { additionalInfo?: { body?: ... | ...; title?: ... | ...; }; adoptionStatement?: string; allowedFonts?: string[]; asset?: { alt?: ... | ...; fileId?: ... | ...; filename?: ... | ...; mimeType?: ... | ...; url?: ... | ...; }; dataSource?: string; defaultValue?: | string | { from: string; to: ... | ...; }; description?: string; format?: string; label: string; maxDate?: string; maxFiles?: number; maxLength?: number; minDate?: string; name: string; options?: (... | ...)[]; placeholder?: string; prefillFrom?: string; required: boolean; showRange?: boolean; showTime?: boolean; signatureMode?: "typed" | "drawn"; timezone?: string; type: | "number" | "date" | "file" | "email" | "url" | "select" | "text" | "checkbox" | "textarea" | "phone" | "radio-group" | "multiselect" | "field-array" | "image" | "file-download" | "section-header" | "section-unset" | "signature" | "signature-adopted"; }[]; format?: string; label: string; maxDate?: string; maxFiles?: number; maxLength?: number; maxRows?: number; minDate?: string; minRows?: number; name: string; options?: ( | string | { description?: string; value: string; })[]; placeholder?: string; prefillFrom?: string; required: boolean; showRange?: boolean; showTime?: boolean; signatureMode?: "typed" | "drawn"; timezone?: string; type: | "number" | "date" | "file" | "email" | "url" | "select" | "text" | "checkbox" | "textarea" | "phone" | "radio-group" | "multiselect" | "field-array" | "image" | "file-download" | "section-header" | "section-unset" | "signature" | "signature-adopted"; }[]; formSchemaVersionId: string; id: string; key: string; label: string | null; }[]; transitions: { fromStepId: string; id: string; label: string | null; priority: number; ruleId: string | null; ruleVersionId: string | null; toStepId: string; }[]; } |
input.attachedRuleIds? | Set<string> |
input.entryStepKey | string | null |
input.rules | { createdAt: Date; createdBy: string | null; currentVersionId: string | null; deletedAt: Date | null; deletedBy: string | null; description: string | null; domain: string; id: string; identifier: string; module: string | null; name: string; organizationId: string; rateTierId: string | null; scheduleConditionId: string | null; status: RuleStatus; statusUpdatedAt: Date; statusUpdatedBy: string | null; updatedAt: Date; updatedBy: string | null; }[] |
input.steps | { description: string | null; fields: { additionalInfo?: { body?: string; title?: string; }; adoptionStatement?: string; allowedFonts?: string[]; asset?: { alt?: string; fileId?: string; filename?: string; mimeType?: string; url?: string; }; dataSource?: string; defaultValue?: | string | { from: string; to: string | null; }; description?: string; fields?: { additionalInfo?: { body?: ... | ...; title?: ... | ...; }; adoptionStatement?: string; allowedFonts?: string[]; asset?: { alt?: ... | ...; fileId?: ... | ...; filename?: ... | ...; mimeType?: ... | ...; url?: ... | ...; }; dataSource?: string; defaultValue?: | string | { from: string; to: ... | ...; }; description?: string; format?: string; label: string; maxDate?: string; maxFiles?: number; maxLength?: number; minDate?: string; name: string; options?: (... | ...)[]; placeholder?: string; prefillFrom?: string; required: boolean; showRange?: boolean; showTime?: boolean; signatureMode?: "typed" | "drawn"; timezone?: string; type: | "number" | "date" | "file" | "email" | "url" | "select" | "text" | "checkbox" | "textarea" | "phone" | "radio-group" | "multiselect" | "field-array" | "image" | "file-download" | "section-header" | "section-unset" | "signature" | "signature-adopted"; }[]; format?: string; label: string; maxDate?: string; maxFiles?: number; maxLength?: number; maxRows?: number; minDate?: string; minRows?: number; name: string; options?: ( | string | { description?: string; value: string; })[]; placeholder?: string; prefillFrom?: string; required: boolean; showRange?: boolean; showTime?: boolean; signatureMode?: "typed" | "drawn"; timezone?: string; type: | "number" | "date" | "file" | "email" | "url" | "select" | "text" | "checkbox" | "textarea" | "phone" | "radio-group" | "multiselect" | "field-array" | "image" | "file-download" | "section-header" | "section-unset" | "signature" | "signature-adopted"; }[]; formSchemaVersionId: string; id: string; key: string; label: string | null; }[] |
input.transitions | { fromStepId: string; id: string; label: string | null; priority: number; ruleId: string | null; ruleVersionId: string | null; toStepId: string; }[] |