Files
2026-05-20 19:58:32 -06:00

232 lines
7.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { z } from "zod";
import { FLOW_STEP_ORDER } from "../../../app/(app)/create/utils/flowSteps";
import { customMethodCardFieldBlocksByIdSchema } from "../../../lib/create/customMethodCardFieldBlocks";
import { MAX_STAKEHOLDER_EMAILS } from "../../../lib/create/stakeholderLimits";
import { assertPlainJsonValue, DEFAULT_PLAIN_JSON_LIMITS } from "./plainJson";
const flowStepTuple = FLOW_STEP_ORDER as unknown as [string, ...string[]];
const createFlowStepSchema = z.enum(flowStepTuple);
const communityStructureChipSnapshotRowSchema = z.object({
id: z.string().max(200),
label: z.string().max(2000),
state: z.string().max(32).optional(),
});
const communityStructureChipSnapshotsSchema = z
.object({
organizationTypes: z.array(communityStructureChipSnapshotRowSchema).optional(),
scale: z.array(communityStructureChipSnapshotRowSchema).optional(),
maturity: z.array(communityStructureChipSnapshotRowSchema).optional(),
})
.strict();
const coreValueDetailEntrySchema = z.object({
meaning: z.string().max(8000),
signals: z.string().max(8000),
});
/**
* Final-review edit modal details per group, merged onto preset defaults at
* publish time. Shapes mirror the custom-rule add-method modals.
*/
const communicationMethodDetailEntrySchema = z.object({
corePrinciple: z.string().max(8000),
logisticsAdmin: z.string().max(8000),
codeOfConduct: z.string().max(8000),
});
const membershipMethodDetailEntrySchema = z.object({
eligibility: z.string().max(8000),
joiningProcess: z.string().max(8000),
expectations: z.string().max(8000),
});
const decisionApproachDetailEntrySchema = z.object({
corePrinciple: z.string().max(8000),
applicableScope: z.array(z.string().max(2000)).max(50),
selectedApplicableScope: z.array(z.string().max(2000)).max(50),
stepByStepInstructions: z.string().max(8000),
consensusLevel: z.number().int().min(0).max(100),
objectionsDeadlocks: z.string().max(8000),
});
const conflictManagementDetailEntrySchema = z.object({
corePrinciple: z.string().max(8000),
applicableScope: z.array(z.string().max(2000)).max(50),
selectedApplicableScope: z.array(z.string().max(2000)).max(50),
processProtocol: z.string().max(8000),
restorationFallbacks: z.string().max(8000),
});
const customMethodCardMetaEntrySchema = z.object({
label: z.string().max(48),
supportText: z.string().max(48),
});
/** Normalized (trim + lowercase) stakeholder email for drafts + publish. */
const stakeholderEmailSchema = z
.string()
.max(320)
.transform((s) => s.trim().toLowerCase())
.pipe(z.string().email());
export { MAX_STAKEHOLDER_EMAILS } from "../../../lib/create/stakeholderLimits";
/**
* Published rule `document` column: arbitrary JSON object with safety bounds.
*/
export const publishedRuleDocumentSchema = z
.record(z.string(), z.unknown())
.superRefine((doc, ctx) => {
const err = assertPlainJsonValue(doc, 0, DEFAULT_PLAIN_JSON_LIMITS);
if (err) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: err,
});
}
});
/**
* Create-flow draft payload: known optional fields plus passthrough for future steps.
* Full tree must satisfy {@link assertPlainJsonValue}.
*/
export const createFlowStateSchema = z
.object({
title: z.string().max(500).optional(),
summary: z.string().max(8000).optional(),
communityContext: z.string().max(200).optional(),
communitySaveEmail: z.string().max(320).optional(),
communityAvatarUrl: z.string().max(512).optional(),
selectedCommunitySizeIds: z.array(z.string()).optional(),
selectedOrganizationTypeIds: z.array(z.string()).optional(),
selectedScaleIds: z.array(z.string()).optional(),
selectedMaturityIds: z.array(z.string()).optional(),
communityStructureChipSnapshots:
communityStructureChipSnapshotsSchema.optional(),
selectedCoreValueIds: z.array(z.string()).max(200).optional(),
coreValuesChipsSnapshot: z
.array(communityStructureChipSnapshotRowSchema)
.optional(),
coreValueDetailsByChipId: z
.record(coreValueDetailEntrySchema)
.optional(),
selectedCommunicationMethodIds: z.array(z.string()).max(200).optional(),
selectedMembershipMethodIds: z.array(z.string()).max(200).optional(),
selectedDecisionApproachIds: z.array(z.string()).max(200).optional(),
selectedConflictManagementIds: z.array(z.string()).max(200).optional(),
communicationMethodDetailsById: z
.record(communicationMethodDetailEntrySchema)
.optional(),
membershipMethodDetailsById: z
.record(membershipMethodDetailEntrySchema)
.optional(),
decisionApproachDetailsById: z
.record(decisionApproachDetailEntrySchema)
.optional(),
conflictManagementDetailsById: z
.record(conflictManagementDetailEntrySchema)
.optional(),
customMethodCardMetaById: z
.record(z.string().max(80), customMethodCardMetaEntrySchema)
.optional(),
customMethodCardFieldBlocksById: customMethodCardFieldBlocksByIdSchema,
methodSectionsPinCommitted: z
.object({
communication: z.boolean().optional(),
membership: z.boolean().optional(),
decisionApproaches: z.boolean().optional(),
conflictManagement: z.boolean().optional(),
})
.strict()
.optional(),
pendingTemplateAction: z
.object({
slug: z.string().max(200),
mode: z.enum(["customize", "useWithoutChanges"]),
})
.strict()
.optional(),
templateReviewBackSlug: z.string().max(200).optional(),
templateReviewEntryFromCreateFlow: z.boolean().optional(),
editingPublishedRuleId: z.string().max(200).optional(),
currentStep: createFlowStepSchema.optional(),
sections: z.array(z.unknown()).optional(),
stakeholderEmails: z
.array(stakeholderEmailSchema)
.max(MAX_STAKEHOLDER_EMAILS)
.optional(),
})
.passthrough()
.superRefine((data, ctx) => {
const err = assertPlainJsonValue(data, 0, DEFAULT_PLAIN_JSON_LIMITS);
if (err) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: err });
}
});
export const publishRuleBodySchema = z.object({
title: z
.string()
.max(500)
.transform((s) => s.trim())
.refine((s) => s.length > 0, { message: "title required" }),
summary: z
.union([z.string().max(8000), z.null()])
.optional()
.transform((val) => {
if (val === undefined || val === null) {
return null;
}
const t = val.trim();
return t.length > 0 ? t : null;
}),
document: publishedRuleDocumentSchema,
stakeholderEmails: z
.array(stakeholderEmailSchema)
.max(MAX_STAKEHOLDER_EMAILS)
.optional(),
});
export type PublishRuleBody = z.infer<typeof publishRuleBodySchema>;
export const postRuleStakeholderBodySchema = z.object({
email: stakeholderEmailSchema,
});
export type PostRuleStakeholderBody = z.infer<
typeof postRuleStakeholderBodySchema
>;
/** Dedupe and drop the publishers own email (`emails` need not be pre-normalized). */
export function uniqueStakeholderEmailsForPublish(
emails: string[] | undefined,
publisherEmailNormalized: string,
): string[] {
if (!emails?.length) return [];
const pub = publisherEmailNormalized.trim().toLowerCase();
const seen = new Set<string>();
const out: string[] = [];
for (const raw of emails) {
const e = raw.trim().toLowerCase();
if (e === pub || seen.has(e)) continue;
seen.add(e);
out.push(e);
}
return out;
}
export const putDraftBodySchema = z.object({
payload: createFlowStateSchema,
});
export type CreateFlowStateValidated = z.infer<typeof createFlowStateSchema>;
export const magicLinkRequestBodySchema = z.object({
email: z.string(),
next: z.string().optional(),
draft: createFlowStateSchema.optional(),
});
export type MagicLinkRequestBody = z.infer<typeof magicLinkRequestBodySchema>;