feat: compose TechLog static and mock adapters
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
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),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user