Files
clean-architecture-frontend…/scripts/lib/validated-json-artifact.ts
T

99 lines
3.0 KiB
TypeScript

import { randomUUID } from "node:crypto";
import {
open as openFile,
rename as renameFile,
rm as removeFile,
} from "node:fs/promises";
import path from "node:path";
import type { z } from "zod";
export type ValidatedJsonArtifactInput = Readonly<{
path: string;
schema: z.ZodType;
value: unknown;
}>;
export type ValidatedJsonArtifactFileSystem = Readonly<{
open: (path: string, flags: "wx") => Promise<{
writeFile(data: string, encoding: "utf8"): Promise<unknown>;
close(): Promise<unknown>;
}>;
rename: (source: string, destination: string) => Promise<unknown>;
rm: (path: string, options: Readonly<{ force: true }>) => Promise<unknown>;
}>;
type ValidatedJsonArtifactWriterDependencies = Readonly<{
createNonce?: () => string;
fileSystem?: ValidatedJsonArtifactFileSystem;
}>;
const defaultFileSystem: ValidatedJsonArtifactFileSystem = Object.freeze({
open: async (target, flags) => openFile(target, flags),
rename: async (source, destination) => renameFile(source, destination),
rm: async (target, options) => removeFile(target, options),
});
/**
* Builds a writer whose only publish operation is an atomic sibling rename.
* Dependency injection is limited to the file-system boundary so failure
* ownership can be verified without exposing a caller-selected cleanup path.
*/
export function createValidatedJsonArtifactWriter(
dependencies: ValidatedJsonArtifactWriterDependencies = {},
) {
const createNonce = dependencies.createNonce ?? randomUUID;
const fileSystem = dependencies.fileSystem ?? defaultFileSystem;
return async function writeArtifact(
input: ValidatedJsonArtifactInput,
): Promise<void> {
const parsed = input.schema.parse(input.value);
const serialized = JSON.stringify(parsed, null, 2);
if (serialized === undefined) {
throw new TypeError("Validated JSON artifact is not serializable");
}
const temporaryPath = path.join(
path.dirname(input.path),
`.${path.basename(input.path)}.${createNonce()}.tmp`,
);
let ownsTemporaryFile = false;
try {
const handle = await fileSystem.open(temporaryPath, "wx");
ownsTemporaryFile = true;
let writeFailed = false;
let writeFailure: unknown;
try {
await handle.writeFile(`${serialized}\n`, "utf8");
} catch (error) {
writeFailed = true;
writeFailure = error;
}
let closeFailed = false;
let closeFailure: unknown;
try {
await handle.close();
} catch (error) {
closeFailed = true;
closeFailure = error;
}
if (writeFailed) throw writeFailure;
if (closeFailed) throw closeFailure;
await fileSystem.rename(temporaryPath, input.path);
} catch (error) {
if (ownsTemporaryFile) {
try {
await fileSystem.rm(temporaryPath, { force: true });
} catch {
// Preserve the publishing failure; cleanup is confined to our nonce.
}
}
throw error;
}
};
}
export const writeValidatedJsonArtifact =
createValidatedJsonArtifactWriter();