232 lines
7.7 KiB
TypeScript
232 lines
7.7 KiB
TypeScript
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 publisher’s 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>;
|