133 lines
4.2 KiB
TypeScript
133 lines
4.2 KiB
TypeScript
import type { components } from "../../contracts/studio/generated.ts";
|
|
import type {
|
|
PublicationAggregate,
|
|
PublicPreview,
|
|
ValidationReport,
|
|
WorkingCopy,
|
|
WorkingCopyDetail,
|
|
} from "../../contracts/studio/contract.ts";
|
|
|
|
export type ValidationResultState = "NOT_RUN" | "INVALID" | "WARNINGS" | "VALID";
|
|
export type ValidationFreshness = "NONE" | "CURRENT" | "STALE";
|
|
export type PreviewState = "NONE" | "CURRENT" | "STALE" | "EXPIRED";
|
|
export type NextAction = components["schemas"]["NextAction"];
|
|
|
|
export type ValidationState = {
|
|
result: ValidationResultState;
|
|
freshness: ValidationFreshness;
|
|
};
|
|
|
|
export type StudioDocumentState = {
|
|
validation: ValidationState;
|
|
preview: PreviewState;
|
|
nextAction: NextAction;
|
|
};
|
|
|
|
export type DocumentStateInput = {
|
|
// eslint-disable-next-line no-restricted-globals -- Exact Studio contract field.
|
|
document: WorkingCopy;
|
|
validation: ValidationReport | null;
|
|
preview: PublicPreview | null;
|
|
publication: PublicationAggregate | null;
|
|
dependencyRevision: string;
|
|
now: Date | string;
|
|
};
|
|
|
|
export type WorkingCopyDetailStateInput = WorkingCopyDetail & {
|
|
now: Date | string;
|
|
};
|
|
|
|
type StateInput = DocumentStateInput | WorkingCopyDetailStateInput;
|
|
|
|
function normalize(input: StateInput): DocumentStateInput {
|
|
if ("validation" in input) return input;
|
|
|
|
return {
|
|
document: input.document,
|
|
validation: input.currentValidation,
|
|
preview: input.latestPreview,
|
|
publication: input.currentPublication,
|
|
dependencyRevision: input.dependencyRevision,
|
|
now: input.now,
|
|
};
|
|
}
|
|
|
|
function timestamp(value: Date | string): number {
|
|
return value instanceof Date ? value.getTime() : Date.parse(value);
|
|
}
|
|
|
|
function isStrictlyFuture(value: string, now: Date | string): boolean {
|
|
const expiry = timestamp(value);
|
|
const current = timestamp(now);
|
|
return Number.isFinite(expiry) && Number.isFinite(current) && current < expiry;
|
|
}
|
|
|
|
function isExpired(value: string, now: Date | string): boolean {
|
|
const expiry = timestamp(value);
|
|
const current = timestamp(now);
|
|
return Number.isFinite(expiry) && Number.isFinite(current) && current >= expiry;
|
|
}
|
|
|
|
export function deriveValidationState(input: StateInput): ValidationState {
|
|
const { document, validation, dependencyRevision, now } = normalize(input);
|
|
if (validation === null) {
|
|
return { result: "NOT_RUN", freshness: "NONE" };
|
|
}
|
|
|
|
const freshness: ValidationFreshness =
|
|
validation.validatedVersion === document.version &&
|
|
isStrictlyFuture(validation.validUntil, now) &&
|
|
validation.dependencyRevision === dependencyRevision
|
|
? "CURRENT"
|
|
: "STALE";
|
|
|
|
return { result: validation.status, freshness };
|
|
}
|
|
|
|
export function derivePreviewState(input: StateInput): PreviewState {
|
|
const normalized = normalize(input);
|
|
const { document, validation, preview, now } = normalized;
|
|
if (preview === null) return "NONE";
|
|
if (isExpired(preview.expiresAt, now)) return "EXPIRED";
|
|
|
|
return preview.previewVersion === document.version &&
|
|
validation !== null &&
|
|
preview.validationId === validation.validationId &&
|
|
deriveValidationState(normalized).freshness === "CURRENT"
|
|
? "CURRENT"
|
|
: "STALE";
|
|
}
|
|
|
|
function isCompletelyEmpty(document: WorkingCopy): boolean {
|
|
return [document.title, document.slug, document.summary].every(
|
|
(value) => value.trim().length === 0,
|
|
);
|
|
}
|
|
|
|
export function deriveNextAction(input: StateInput): NextAction {
|
|
const normalized = normalize(input);
|
|
const { document, publication } = normalized;
|
|
|
|
if (publication?.status === "PUBLISHED" && publication.publishedVersion === document.version) {
|
|
return "NONE";
|
|
}
|
|
if (isCompletelyEmpty(document)) return "CONTINUE_EDITING";
|
|
|
|
const validation = deriveValidationState(normalized);
|
|
if (validation.freshness !== "CURRENT") return "VALIDATE";
|
|
if (validation.result === "INVALID") return "FIX_VALIDATION";
|
|
|
|
const preview = derivePreviewState(normalized);
|
|
if (preview !== "CURRENT") return "CREATE_PREVIEW";
|
|
return "PUBLISH";
|
|
}
|
|
|
|
export function deriveDocumentState(input: StateInput): StudioDocumentState {
|
|
const normalized = normalize(input);
|
|
return {
|
|
validation: deriveValidationState(normalized),
|
|
preview: derivePreviewState(normalized),
|
|
nextAction: deriveNextAction(normalized),
|
|
};
|
|
}
|