Improve type safety of tool definitions (#6895)

# Description of Changes
Followup work requested in review of #6867. Currently, there is nothing
enforcing that the endpoint chosen in the tool config is the correct
mapping for `toApiParams`, so theoretically it's possible for a tool to
be set up to call an endpoint with the wrong API params for it. There's
also nothing currently enforcing that `toApiParams` and `fromApiParams`
are compatible with each other (using the same types). This PR changes
it so that instead of creating the config object directly, tools create
it via a generic function, which enforces that all of the relevant
mappings are using compatible types.
This commit is contained in:
James Brunton
2026-07-07 16:43:06 +00:00
committed by GitHub
parent 8ba8f69252
commit be57f11747
46 changed files with 239 additions and 249 deletions
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import {
ToolType,
useToolOperation,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import {
objectToFormData,
@@ -73,15 +73,14 @@ export const buildAddPageNumbersFormData = (
): FormData =>
objectToFormData(addPageNumbersToApiParams(parameters), { fileInput: file });
export const addPageNumbersOperationConfig = {
toolType: ToolType.singleFile,
export const addPageNumbersOperationConfig = defineSingleFileTool({
buildFormData: buildAddPageNumbersFormData,
toApiParams: addPageNumbersToApiParams,
fromApiParams: addPageNumbersFromApiParams,
operationType: "addPageNumbers",
endpoint: ENDPOINT,
defaultParameters,
} as const;
});
export const useAddPageNumbersOperation = () => {
const { t } = useTranslation();
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import {
ToolType,
useToolOperation,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import {
objectToFormData,
@@ -87,15 +87,14 @@ export const buildAddStampFormData = (
: { fileInput: file },
);
export const addStampOperationConfig = {
toolType: ToolType.singleFile,
export const addStampOperationConfig = defineSingleFileTool({
buildFormData: buildAddStampFormData,
toApiParams: addStampToApiParams,
fromApiParams: addStampFromApiParams,
operationType: "addStamp",
endpoint: ENDPOINT,
defaultParameters,
} as const;
});
export const useAddStampOperation = () => {
const { t } = useTranslation();
@@ -1,8 +1,7 @@
import { useTranslation } from "react-i18next";
import {
useToolOperation,
ToolOperationConfig,
ToolType,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import {
objectToFormData,
@@ -48,16 +47,14 @@ const buildFormData = (
});
// Operation configuration for automation
export const addAttachmentsOperationConfig: ToolOperationConfig<AddAttachmentsParameters> =
{
toolType: ToolType.singleFile,
buildFormData,
toApiParams: addAttachmentsToApiParams,
fromApiParams: addAttachmentsFromApiParams,
operationType: "addAttachments",
endpoint: ENDPOINT,
defaultParameters: DEFAULT_ADD_ATTACHMENTS_PARAMETERS,
};
export const addAttachmentsOperationConfig = defineSingleFileTool({
buildFormData,
toApiParams: addAttachmentsToApiParams,
fromApiParams: addAttachmentsFromApiParams,
operationType: "addAttachments",
endpoint: ENDPOINT,
defaultParameters: DEFAULT_ADD_ATTACHMENTS_PARAMETERS,
});
export const useAddAttachmentsOperation = () => {
const { t } = useTranslation();
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import {
ToolType,
useToolOperation,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import {
objectToFormData,
@@ -85,15 +85,14 @@ const fullDefaultParameters: AddPasswordFullParameters = {
};
// Static configuration object
export const addPasswordOperationConfig = {
toolType: ToolType.singleFile,
export const addPasswordOperationConfig = defineSingleFileTool({
buildFormData: buildAddPasswordFormData,
toApiParams: addPasswordToApiParams,
fromApiParams: addPasswordFromApiParams,
operationType: "addPassword",
endpoint: ENDPOINT,
defaultParameters: fullDefaultParameters,
} as const;
});
export const useAddPasswordOperation = () => {
const { t } = useTranslation();
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import {
ToolType,
useToolOperation,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import {
objectToFormData,
@@ -87,15 +87,14 @@ export const buildAddWatermarkFormData = (
);
// Static configuration object
export const addWatermarkOperationConfig = {
toolType: ToolType.singleFile,
export const addWatermarkOperationConfig = defineSingleFileTool({
buildFormData: buildAddWatermarkFormData,
toApiParams: addWatermarkToApiParams,
fromApiParams: addWatermarkFromApiParams,
operationType: "watermark",
endpoint: ENDPOINT,
defaultParameters,
} as const;
});
export const useAddWatermarkOperation = () => {
const { t } = useTranslation();
@@ -1,6 +1,6 @@
import { useTranslation } from "react-i18next";
import {
ToolType,
defineCustomTool,
useToolOperation,
CustomProcessorResult,
} from "@app/hooks/tools/shared/useToolOperation";
@@ -195,14 +195,11 @@ async function processPdfClientSide(
};
}
export const adjustContrastOperationConfig = {
toolType: ToolType.custom,
export const adjustContrastOperationConfig = defineCustomTool({
customProcessor: processPdfClientSide,
operationType: "adjustContrast",
defaultParameters,
settingsComponentPath:
"components/tools/adjustContrast/AdjustContrastSingleStepSettings",
} as const;
});
export const useAdjustContrastOperation = () => {
const { t } = useTranslation();
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import {
useToolOperation,
ToolType,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import { createStandardErrorHandler } from "@app/utils/toolErrorHandler";
import {
@@ -21,15 +21,14 @@ export {
adjustPageScaleFromApiParams,
};
export const adjustPageScaleOperationConfig = {
toolType: ToolType.singleFile,
export const adjustPageScaleOperationConfig = defineSingleFileTool({
buildFormData: buildAdjustPageScaleFormData,
toApiParams: adjustPageScaleToApiParams,
fromApiParams: adjustPageScaleFromApiParams,
operationType: "scalePages",
endpoint: ADJUST_PAGE_SCALE_ENDPOINT,
defaultParameters,
} as const;
});
export const useAdjustPageScaleOperation = () => {
const { t } = useTranslation();
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import {
ToolType,
useToolOperation,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import {
objectToFormData,
@@ -44,8 +44,7 @@ export const buildAutoRenameFormData = (
objectToFormData(autoRenameToApiParams(parameters), { fileInput: file });
// Static configuration object
export const autoRenameOperationConfig = {
toolType: ToolType.singleFile,
export const autoRenameOperationConfig = defineSingleFileTool({
buildFormData: buildAutoRenameFormData,
toApiParams: autoRenameToApiParams,
fromApiParams: autoRenameFromApiParams,
@@ -53,7 +52,7 @@ export const autoRenameOperationConfig = {
endpoint: ENDPOINT,
preserveBackendFilename: true, // Use filename from backend response headers
defaultParameters,
} as const;
});
export const useAutoRenameOperation = () => {
const { t } = useTranslation();
@@ -1,5 +1,5 @@
import {
ToolType,
defineCustomTool,
useToolOperation,
} from "@app/hooks/tools/shared/useToolOperation";
import { useCallback } from "react";
@@ -55,10 +55,11 @@ export function useAutomateOperation() {
[toolRegistry],
);
return useToolOperation<AutomateParameters>({
toolType: ToolType.custom,
operationType: "automate",
customProcessor,
consumesAllInputs: true,
});
return useToolOperation<AutomateParameters>(
defineCustomTool({
operationType: "automate",
customProcessor,
consumesAllInputs: true,
}),
);
}
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import {
useToolOperation,
ToolType,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import {
objectToFormData,
@@ -59,15 +59,14 @@ export const buildBookletImpositionFormData = (
});
// Static configuration object
export const bookletImpositionOperationConfig = {
toolType: ToolType.singleFile,
export const bookletImpositionOperationConfig = defineSingleFileTool({
buildFormData: buildBookletImpositionFormData,
toApiParams: bookletImpositionToApiParams,
fromApiParams: bookletImpositionFromApiParams,
operationType: "bookletImposition",
endpoint: ENDPOINT,
defaultParameters,
} as const;
});
export const useBookletImpositionOperation = () => {
const { t } = useTranslation();
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import {
ToolType,
useToolOperation,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import {
objectToFormData,
@@ -136,16 +136,14 @@ export const buildCertSignFormData = (
});
// Static configuration object
export const certSignOperationConfig = {
toolType: ToolType.singleFile,
export const certSignOperationConfig = defineSingleFileTool({
buildFormData: buildCertSignFormData,
toApiParams: certSignToApiParams,
fromApiParams: certSignFromApiParams,
operationType: "certSign",
endpoint: ENDPOINT,
multiFileEndpoint: false,
defaultParameters,
} as const;
});
export const useCertSignOperation = () => {
const { t } = useTranslation();
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import {
useToolOperation,
ToolType,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import { createStandardErrorHandler } from "@app/utils/toolErrorHandler";
import {
@@ -75,13 +75,12 @@ export const buildChangeMetadataFormData = (
};
// Static configuration object
export const changeMetadataOperationConfig = {
toolType: ToolType.singleFile,
export const changeMetadataOperationConfig = defineSingleFileTool({
buildFormData: buildChangeMetadataFormData,
operationType: "changeMetadata",
endpoint: "/api/v1/misc/update-metadata",
defaultParameters,
} as const;
});
export const useChangeMetadataOperation = () => {
const { t } = useTranslation();
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import {
ToolType,
useToolOperation,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import {
objectToFormData,
@@ -71,15 +71,14 @@ export const buildChangePermissionsFormData = (
});
// Static configuration object
export const changePermissionsOperationConfig = {
toolType: ToolType.singleFile,
export const changePermissionsOperationConfig = defineSingleFileTool({
buildFormData: buildChangePermissionsFormData,
toApiParams: changePermissionsToApiParams,
fromApiParams: changePermissionsFromApiParams,
operationType: "changePermissions",
endpoint: ENDPOINT, // Change Permissions is a fake endpoint for the Add Password tool
defaultParameters,
} as const;
});
export const useChangePermissionsOperation = () => {
const { t } = useTranslation();
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import {
useToolOperation,
ToolType,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import {
objectToFormData,
@@ -90,15 +90,14 @@ export const buildCompressFormData = (
objectToFormData(compressToApiParams(parameters), { fileInput: file });
// Static configuration object
export const compressOperationConfig = {
toolType: ToolType.singleFile,
export const compressOperationConfig = defineSingleFileTool({
buildFormData: buildCompressFormData,
toApiParams: compressToApiParams,
fromApiParams: compressFromApiParams,
operationType: "compress",
endpoint: ENDPOINT,
defaultParameters,
} as const;
});
export const useCompressOperation = () => {
const { t } = useTranslation();
@@ -8,7 +8,7 @@ import {
import { createFileFromApiResponse } from "@app/utils/fileResponseUtils";
import {
useToolOperation,
ToolType,
defineCustomTool,
CustomProcessorResult,
} from "@app/hooks/tools/shared/useToolOperation";
import {
@@ -300,8 +300,7 @@ export const convertProcessor = async (
};
// Static configuration object
export const convertOperationConfig = {
toolType: ToolType.custom,
export const convertOperationConfig = defineCustomTool({
customProcessor: convertProcessor, // Can't use callback version here
operationType: "convert",
defaultParameters,
@@ -311,7 +310,7 @@ export const convertOperationConfig = {
params.toExtension === "pdfx" ? "pdfa" : params.toExtension;
return getEndpointUrl(params.fromExtension, actualToExtension) ?? undefined;
},
} as const;
});
export const useConvertOperation = (parameters?: ConvertParameters) => {
const { t } = useTranslation();
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import {
useToolOperation,
ToolType,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import {
objectToFormData,
@@ -59,15 +59,14 @@ export const buildCropFormData = (
objectToFormData(cropToApiParams(parameters), { fileInput: file });
// Static configuration object
export const cropOperationConfig = {
toolType: ToolType.singleFile,
export const cropOperationConfig = defineSingleFileTool({
buildFormData: buildCropFormData,
toApiParams: cropToApiParams,
fromApiParams: cropFromApiParams,
operationType: "crop",
endpoint: ENDPOINT,
defaultParameters,
} as const;
});
export const useCropOperation = () => {
const { t } = useTranslation();
@@ -1,7 +1,6 @@
import { useTranslation } from "react-i18next";
import {
ToolType,
type ToolOperationConfig,
defineSingleFileTool,
useToolOperation,
} from "@app/hooks/tools/shared/useToolOperation";
import {
@@ -64,15 +63,13 @@ const buildFormData = (
fileInput: file,
});
export const editTableOfContentsOperationConfig: ToolOperationConfig<EditTableOfContentsParameters> =
{
toolType: ToolType.singleFile,
operationType: "editTableOfContents",
endpoint: ENDPOINT,
buildFormData,
toApiParams: editTableOfContentsToApiParams,
fromApiParams: editTableOfContentsFromApiParams,
};
export const editTableOfContentsOperationConfig = defineSingleFileTool({
operationType: "editTableOfContents",
endpoint: ENDPOINT,
buildFormData,
toApiParams: editTableOfContentsToApiParams,
fromApiParams: editTableOfContentsFromApiParams,
});
export const useEditTableOfContentsOperation = () => {
const { t } = useTranslation();
@@ -2,7 +2,7 @@ import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import {
useToolOperation,
ToolType,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import {
objectToFormData,
@@ -41,15 +41,14 @@ export const buildExtractImagesFormData = (
objectToFormData(extractImagesToApiParams(parameters), { fileInput: file });
// Static configuration object (without response handler - will be added in hook)
export const extractImagesOperationConfig = {
toolType: ToolType.singleFile,
export const extractImagesOperationConfig = defineSingleFileTool({
buildFormData: buildExtractImagesFormData,
toApiParams: extractImagesToApiParams,
fromApiParams: extractImagesFromApiParams,
operationType: "extractImages",
endpoint: ENDPOINT,
defaultParameters,
} as const;
});
export const useExtractImagesOperation = () => {
const { t } = useTranslation();
@@ -1,7 +1,7 @@
import apiClient from "@app/services/apiClient";
import { useTranslation } from "react-i18next";
import {
ToolType,
defineCustomTool,
useToolOperation,
CustomProcessorResult,
} from "@app/hooks/tools/shared/useToolOperation";
@@ -33,8 +33,7 @@ async function resolveSelectionToCsv(
}
}
export const extractPagesOperationConfig = {
toolType: ToolType.custom,
export const extractPagesOperationConfig = defineCustomTool({
operationType: "extractPages",
customProcessor: async (
parameters: ExtractPagesParameters,
@@ -71,7 +70,7 @@ export const extractPagesOperationConfig = {
};
},
defaultParameters,
} as const;
});
export const useExtractPagesOperation = () => {
const { t } = useTranslation();
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import {
ToolType,
useToolOperation,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import {
objectToFormData,
@@ -59,16 +59,14 @@ export const buildFlattenFormData = (
objectToFormData(flattenToApiParams(parameters), { fileInput: file });
// Static configuration object
export const flattenOperationConfig = {
toolType: ToolType.singleFile,
export const flattenOperationConfig = defineSingleFileTool({
buildFormData: buildFlattenFormData,
toApiParams: flattenToApiParams,
fromApiParams: flattenFromApiParams,
operationType: "flatten",
endpoint: ENDPOINT,
multiFileEndpoint: false,
defaultParameters,
} as const;
});
export const useFlattenOperation = () => {
const { t } = useTranslation();
@@ -1,8 +1,7 @@
import { useTranslation } from "react-i18next";
import {
useToolOperation,
ToolOperationConfig,
ToolType,
defineMultiFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import {
objectToFormData,
@@ -56,8 +55,7 @@ const buildFormData = (
};
// Operation configuration for automation
export const mergeOperationConfig: ToolOperationConfig<MergeParameters> = {
toolType: ToolType.multiFile,
export const mergeOperationConfig = defineMultiFileTool({
buildFormData,
toApiParams: mergeToApiParams,
fromApiParams: mergeFromApiParams,
@@ -65,7 +63,7 @@ export const mergeOperationConfig: ToolOperationConfig<MergeParameters> = {
endpoint: ENDPOINT,
filePrefix: "merged_",
defaultParameters,
};
});
export const useMergeOperation = () => {
const { t } = useTranslation();
@@ -7,7 +7,7 @@ import {
import {
useToolOperation,
ToolOperationConfig,
ToolType,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import {
objectToFormData,
@@ -151,15 +151,14 @@ export const ocrResponseHandler = async (
};
// Static configuration object (without t function dependencies)
export const ocrOperationConfig = {
toolType: ToolType.singleFile,
export const ocrOperationConfig = defineSingleFileTool({
buildFormData: buildOCRFormData,
toApiParams: ocrToApiParams,
fromApiParams: ocrFromApiParams,
operationType: "ocr",
endpoint: ENDPOINT,
defaultParameters,
} as const;
});
export const useOCROperation = () => {
const { t } = useTranslation();
@@ -1,8 +1,8 @@
import { useTranslation } from "react-i18next";
import {
useToolOperation,
ToolType,
type ToolOperationConfig,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import {
objectToFormData,
@@ -60,14 +60,13 @@ const buildFormData = (
});
export const overlayPdfsOperationConfig: ToolOperationConfig<OverlayPdfsParameters> =
{
toolType: ToolType.singleFile,
defineSingleFileTool({
buildFormData,
toApiParams: overlayPdfsToApiParams,
fromApiParams: overlayPdfsFromApiParams,
operationType: "overlayPdfs",
endpoint: ENDPOINT,
};
});
export const useOverlayPdfsOperation = () => {
const { t } = useTranslation();
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import {
ToolType,
useToolOperation,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import {
objectToFormData,
@@ -67,15 +67,14 @@ export const buildPageLayoutFormData = (
): FormData =>
objectToFormData(pageLayoutToApiParams(parameters), { fileInput: file });
export const pageLayoutOperationConfig = {
toolType: ToolType.singleFile,
export const pageLayoutOperationConfig = defineSingleFileTool({
buildFormData: buildPageLayoutFormData,
toApiParams: pageLayoutToApiParams,
fromApiParams: pageLayoutFromApiParams,
operationType: "pageLayout",
endpoint: ENDPOINT,
defaultParameters,
} as const;
});
export const usePageLayoutOperation = () => {
const { t } = useTranslation();
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import {
useToolOperation,
ToolType,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import {
objectToFormData,
@@ -64,8 +64,7 @@ export const buildRedactFormData = (
};
// Static configuration object
export const redactOperationConfig = {
toolType: ToolType.singleFile,
export const redactOperationConfig = defineSingleFileTool({
buildFormData: buildRedactFormData,
toApiParams: redactToApiParams,
fromApiParams: redactFromApiParams,
@@ -73,7 +72,7 @@ export const redactOperationConfig = {
endpoint: (parameters: RedactParameters) =>
parameters.mode === "automatic" ? AUTO_ENDPOINT : null,
defaultParameters,
} as const;
});
export const useRedactOperation = () => {
const { t } = useTranslation();
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import {
useToolOperation,
ToolType,
defineCustomTool,
CustomProcessorResult,
} from "@app/hooks/tools/shared/useToolOperation";
import { createStandardErrorHandler } from "@app/utils/toolErrorHandler";
@@ -78,12 +78,11 @@ const removeAnnotationsProcessor = async (
};
// Static configuration object
export const removeAnnotationsOperationConfig = {
toolType: ToolType.custom,
export const removeAnnotationsOperationConfig = defineCustomTool({
operationType: "removeAnnotations",
customProcessor: removeAnnotationsProcessor,
defaultParameters,
} as const;
});
export const useRemoveAnnotationsOperation = () => {
const { t } = useTranslation();
@@ -1,9 +1,8 @@
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import {
ToolType,
useToolOperation,
ToolOperationConfig,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import {
objectToFormData,
@@ -41,15 +40,14 @@ export const buildRemoveBlanksFormData = (
): FormData =>
objectToFormData(removeBlanksToApiParams(parameters), { fileInput: file });
export const removeBlanksOperationConfig = {
toolType: ToolType.singleFile,
export const removeBlanksOperationConfig = defineSingleFileTool({
buildFormData: buildRemoveBlanksFormData,
toApiParams: removeBlanksToApiParams,
fromApiParams: removeBlanksFromApiParams,
operationType: "removeBlanks",
endpoint: ENDPOINT,
defaultParameters,
} as const satisfies ToolOperationConfig<RemoveBlanksParameters>;
});
export const useRemoveBlanksOperation = () => {
const { t } = useTranslation();
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import {
ToolType,
useToolOperation,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import {
fileOnlyMapping,
@@ -25,15 +25,14 @@ export const buildRemoveCertificateSignFormData = (
): FormData => objectToFormData(toApiParams(), { fileInput: file });
// Static configuration object
export const removeCertificateSignOperationConfig = {
toolType: ToolType.singleFile,
export const removeCertificateSignOperationConfig = defineSingleFileTool({
buildFormData: buildRemoveCertificateSignFormData,
toApiParams,
fromApiParams,
operationType: "removeCertSign",
endpoint: ENDPOINT,
defaultParameters,
} as const;
});
export const useRemoveCertificateSignOperation = () => {
const { t } = useTranslation();
@@ -1,8 +1,7 @@
import { useTranslation } from "react-i18next";
import {
useToolOperation,
ToolOperationConfig,
ToolType,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import {
fileOnlyMapping,
@@ -22,15 +21,13 @@ export const buildRemoveImageFormData = (
file: File,
): FormData => objectToFormData(toApiParams(), { fileInput: file });
export const removeImageOperationConfig: ToolOperationConfig<RemoveImageParameters> =
{
toolType: ToolType.singleFile,
buildFormData: buildRemoveImageFormData,
toApiParams,
fromApiParams,
operationType: "removeImage",
endpoint: ENDPOINT,
};
export const removeImageOperationConfig = defineSingleFileTool({
buildFormData: buildRemoveImageFormData,
toApiParams,
fromApiParams,
operationType: "removeImage",
endpoint: ENDPOINT,
});
export const useRemoveImageOperation = () => {
const { t } = useTranslation();
@@ -1,8 +1,7 @@
import { useTranslation } from "react-i18next";
import {
ToolType,
useToolOperation,
ToolOperationConfig,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import {
objectToFormData,
@@ -42,15 +41,14 @@ export const buildRemovePagesFormData = (
): FormData =>
objectToFormData(removePagesToApiParams(parameters), { fileInput: file });
export const removePagesOperationConfig = {
toolType: ToolType.singleFile,
export const removePagesOperationConfig = defineSingleFileTool({
buildFormData: buildRemovePagesFormData,
toApiParams: removePagesToApiParams,
fromApiParams: removePagesFromApiParams,
operationType: "removePages",
endpoint: ENDPOINT,
defaultParameters,
} as const satisfies ToolOperationConfig<RemovePagesParameters>;
});
export const useRemovePagesOperation = () => {
const { t } = useTranslation();
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import {
ToolType,
useToolOperation,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import { createStandardErrorHandler } from "@app/utils/toolErrorHandler";
import {
@@ -19,15 +19,14 @@ import {
export { buildRemovePasswordFormData };
// Static configuration object
export const removePasswordOperationConfig = {
toolType: ToolType.singleFile,
export const removePasswordOperationConfig = defineSingleFileTool({
buildFormData: buildRemovePasswordFormData,
toApiParams: removePasswordToApiParams,
fromApiParams: removePasswordFromApiParams,
operationType: "removePassword",
endpoint: REMOVE_PASSWORD_ENDPOINT,
defaultParameters,
} as const;
});
export const useRemovePasswordOperation = () => {
const { t } = useTranslation();
@@ -1,7 +1,6 @@
import { useTranslation } from "react-i18next";
import {
ToolOperationConfig,
ToolType,
defineSingleFileTool,
useToolOperation,
} from "@app/hooks/tools/shared/useToolOperation";
import {
@@ -54,15 +53,13 @@ const buildFormData = (
fileInput: file,
});
export const reorganizePagesOperationConfig: ToolOperationConfig<ReorganizePagesParameters> =
{
toolType: ToolType.singleFile,
buildFormData,
toApiParams: reorganizePagesToApiParams,
fromApiParams: reorganizePagesFromApiParams,
operationType: "reorganizePages",
endpoint: ENDPOINT,
};
export const reorganizePagesOperationConfig = defineSingleFileTool({
buildFormData,
toApiParams: reorganizePagesToApiParams,
fromApiParams: reorganizePagesFromApiParams,
operationType: "reorganizePages",
endpoint: ENDPOINT,
});
export const useReorganizePagesOperation = () => {
const { t } = useTranslation();
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import {
ToolType,
useToolOperation,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import {
fileOnlyMapping,
@@ -25,15 +25,14 @@ export const buildRepairFormData = (
): FormData => objectToFormData(toApiParams(), { fileInput: file });
// Static configuration object
export const repairOperationConfig = {
toolType: ToolType.singleFile,
export const repairOperationConfig = defineSingleFileTool({
buildFormData: buildRepairFormData,
toApiParams,
fromApiParams,
operationType: "repair",
endpoint: ENDPOINT,
defaultParameters,
} as const;
});
export const useRepairOperation = () => {
const { t } = useTranslation();
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import {
ToolType,
useToolOperation,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import {
objectToFormData,
@@ -62,16 +62,14 @@ export const buildReplaceColorFormData = (
): FormData =>
objectToFormData(replaceColorToApiParams(parameters), { fileInput: file });
export const replaceColorOperationConfig = {
toolType: ToolType.singleFile,
export const replaceColorOperationConfig = defineSingleFileTool({
buildFormData: buildReplaceColorFormData,
toApiParams: replaceColorToApiParams,
fromApiParams: replaceColorFromApiParams,
operationType: "replaceColor",
endpoint: ENDPOINT,
multiFileEndpoint: false,
defaultParameters,
} as const;
});
export const useReplaceColorOperation = () => {
const { t } = useTranslation();
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import {
useToolOperation,
ToolType,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import {
objectToFormData,
@@ -43,15 +43,14 @@ export const buildRotateFormData = (
objectToFormData(rotateToApiParams(parameters), { fileInput: file });
// Static configuration object
export const rotateOperationConfig = {
toolType: ToolType.singleFile,
export const rotateOperationConfig = defineSingleFileTool({
buildFormData: buildRotateFormData,
toApiParams: rotateToApiParams,
fromApiParams: rotateFromApiParams,
operationType: "rotate",
endpoint: ENDPOINT,
defaultParameters,
} as const;
});
export const useRotateOperation = () => {
const { t } = useTranslation();
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import {
ToolType,
useToolOperation,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import {
objectToFormData,
@@ -55,16 +55,14 @@ export const buildSanitizeFormData = (
objectToFormData(sanitizeToApiParams(parameters), { fileInput: file });
// Static configuration object
export const sanitizeOperationConfig = {
toolType: ToolType.singleFile,
export const sanitizeOperationConfig = defineSingleFileTool({
buildFormData: buildSanitizeFormData,
toApiParams: sanitizeToApiParams,
fromApiParams: sanitizeFromApiParams,
operationType: "sanitize",
endpoint: ENDPOINT,
multiFileEndpoint: false,
defaultParameters,
} as const;
});
export const useSanitizeOperation = () => {
const { t } = useTranslation();
@@ -1,9 +1,9 @@
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import {
ToolType,
useToolOperation,
ToolOperationConfig,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import {
objectToFormData,
@@ -54,15 +54,14 @@ export const buildScannerImageSplitFormData = (
});
// Static configuration object
export const scannerImageSplitOperationConfig = {
toolType: ToolType.singleFile,
export const scannerImageSplitOperationConfig = defineSingleFileTool({
buildFormData: buildScannerImageSplitFormData,
toApiParams: scannerImageSplitToApiParams,
fromApiParams: scannerImageSplitFromApiParams,
operationType: "scannerImageSplit",
endpoint: ENDPOINT,
defaultParameters,
} as const;
});
export const useScannerImageSplitOperation = () => {
const { t } = useTranslation();
@@ -117,7 +117,7 @@ describe("migrated tool mappers (sweep)", () => {
describe("redact mappers", () => {
test("toApiParams builds the auto-redact body from UI parameters", () => {
const api = redactOperationConfig.toApiParams({
const api = redactOperationConfig.toApiParams!({
mode: "automatic",
wordsToRedact: ["foo", "bar"],
useRegex: true,
@@ -138,7 +138,7 @@ describe("redact mappers", () => {
});
test("round-trips through fromApiParams", () => {
const api = redactOperationConfig.toApiParams({
const api = redactOperationConfig.toApiParams!({
mode: "automatic",
wordsToRedact: ["secret"],
useRegex: false,
@@ -147,7 +147,7 @@ describe("redact mappers", () => {
customPadding: 0.1,
convertPDFToImage: true,
});
const roundTripped = redactOperationConfig.toApiParams({
const roundTripped = redactOperationConfig.toApiParams!({
mode: "automatic",
wordsToRedact: [],
useRegex: false,
@@ -155,7 +155,7 @@ describe("redact mappers", () => {
redactColor: "#000000",
customPadding: 0,
convertPDFToImage: false,
...redactOperationConfig.fromApiParams(api),
...redactOperationConfig.fromApiParams!(api),
});
expect(roundTripped).toEqual(api);
@@ -3,7 +3,7 @@ import { StirlingFile } from "@app/types/fileContext";
import type { ResponseHandler } from "@app/utils/toolResponseProcessor";
import { ToolId } from "@app/types/toolId";
import type { ProcessingProgress } from "@app/hooks/tools/shared/useToolState";
import type { ToolApiRequest, ToolEndpoint } from "@app/types/toolApiTypes";
import type { ToolApiParams, ToolEndpoint } from "@app/types/toolApiTypes";
export type { ProcessingProgress, ResponseHandler };
@@ -53,7 +53,7 @@ export interface CustomProcessorResult {
* 2. Multi-file tools: toolType: multiFile, single API call with all files
* 3. Complex tools: toolType: custom, customProcessor handles all processing logic
*/
interface BaseToolOperationConfig<TParams> {
interface BaseToolOperationConfig<TParams, TEndpoint extends ToolEndpoint> {
/** Operation identifier for tracking and logging */
operationType: ToolId;
@@ -82,15 +82,16 @@ interface BaseToolOperationConfig<TParams> {
/**
* Typed frontend params -> backend request model. When a tool provides this,
* it is the spec-checked source of truth for the request body and its
* buildFormData is derived from it via objectToFormData.
* buildFormData is derived from it via objectToFormData. Bound to the tool's
* endpoint, so a spec rename of that endpoint's model breaks the build here.
*/
toApiParams?(params: TParams): ToolApiRequest;
toApiParams?(params: TParams): ToolApiParams[TEndpoint];
/**
* Backend request model -> partial frontend params, so a stored API call
* can be re-hydrated into this tool's settings UI.
*/
fromApiParams?(apiParams: ToolApiRequest): Partial<TParams>;
fromApiParams?(apiParams: ToolApiParams[TEndpoint]): Partial<TParams>;
/**
* For custom tools: if true, success implies all input files were successfully processed.
@@ -102,7 +103,8 @@ interface BaseToolOperationConfig<TParams> {
export interface SingleFileToolOperationConfig<
TParams,
> extends BaseToolOperationConfig<TParams> {
TEndpoint extends ToolEndpoint = ToolEndpoint,
> extends BaseToolOperationConfig<TParams, TEndpoint> {
/** This tool processes one file at a time. */
toolType: ToolType.singleFile;
@@ -110,18 +112,18 @@ export interface SingleFileToolOperationConfig<
buildFormData: (params: TParams, file: File) => FormData;
/**
* API endpoint for the operation. Can be static or a function for dynamic routing.
* API endpoint for the operation, or a function for dynamic routing. `null`
* when the operation has no backend endpoint (see {@link ToolOperationEndpoint}).
*/
endpoint:
| ToolOperationEndpoint
| ((params: TParams) => ToolOperationEndpoint);
endpoint: TEndpoint | null | ((params: TParams) => TEndpoint | null);
customProcessor?: undefined;
}
export interface MultiFileToolOperationConfig<
TParams,
> extends BaseToolOperationConfig<TParams> {
TEndpoint extends ToolEndpoint = ToolEndpoint,
> extends BaseToolOperationConfig<TParams, TEndpoint> {
/** This tool processes multiple files at once. */
toolType: ToolType.multiFile;
@@ -132,18 +134,17 @@ export interface MultiFileToolOperationConfig<
buildFormData: (params: TParams, files: File[]) => FormData;
/**
* API endpoint for the operation. Can be static or a function for dynamic routing.
* API endpoint for the operation, or a function for dynamic routing. `null`
* when the operation has no backend endpoint (see {@link ToolOperationEndpoint}).
*/
endpoint:
| ToolOperationEndpoint
| ((params: TParams) => ToolOperationEndpoint);
endpoint: TEndpoint | null | ((params: TParams) => TEndpoint | null);
customProcessor?: undefined;
}
export interface CustomToolOperationConfig<
TParams,
> extends BaseToolOperationConfig<TParams> {
> extends BaseToolOperationConfig<TParams, ToolEndpoint> {
/** This tool has custom behaviour. */
toolType: ToolType.custom;
@@ -171,11 +172,50 @@ export interface CustomToolOperationConfig<
) => Promise<CustomProcessorResult>;
}
export type ToolOperationConfig<TParams = void> =
| SingleFileToolOperationConfig<TParams>
| MultiFileToolOperationConfig<TParams>
export type ToolOperationConfig<
TParams = void,
TEndpoint extends ToolEndpoint = ToolEndpoint,
> =
| SingleFileToolOperationConfig<TParams, TEndpoint>
| MultiFileToolOperationConfig<TParams, TEndpoint>
| CustomToolOperationConfig<TParams>;
/**
* Define a single-file tool's operation config. Infers the endpoint literal from
* `endpoint` and binds toApiParams/fromApiParams to that endpoint's request
* model, so a mapper cannot silently drift from the generated spec.
*/
export function defineSingleFileTool<
TParams,
const TEndpoint extends ToolEndpoint,
>(
config: Omit<SingleFileToolOperationConfig<TParams, TEndpoint>, "toolType">,
): SingleFileToolOperationConfig<TParams, TEndpoint> {
return { ...config, toolType: ToolType.singleFile };
}
/** Multi-file counterpart of {@link defineSingleFileTool}. */
export function defineMultiFileTool<
TParams,
const TEndpoint extends ToolEndpoint,
>(
config: Omit<MultiFileToolOperationConfig<TParams, TEndpoint>, "toolType">,
): MultiFileToolOperationConfig<TParams, TEndpoint> {
return { ...config, toolType: ToolType.multiFile };
}
/**
* Custom-processor counterpart of {@link defineSingleFileTool}, for tools whose
* customProcessor owns the API calls and file handling. Rejects fields that
* belong to the file-based patterns (e.g. buildFormData) and any property not on
* the config, so a stray or stale field is a build error rather than dead weight.
*/
export function defineCustomTool<TParams>(
config: Omit<CustomToolOperationConfig<TParams>, "toolType">,
): CustomToolOperationConfig<TParams> {
return { ...config, toolType: ToolType.custom };
}
/**
* One generic source-of-truth for the props every automation settings component
* accepts: the tool's parameters plus a typed change handler.
@@ -40,6 +40,9 @@ import {
} from "@app/hooks/tools/shared/toolOperationHelpers";
import {
ToolType,
defineSingleFileTool,
defineMultiFileTool,
defineCustomTool,
ToolOperationConfig,
ToolOperationHook,
CustomProcessorResult,
@@ -50,7 +53,12 @@ import {
ResponseHandler,
} from "@app/hooks/tools/shared/toolOperationTypes";
export { ToolType };
export {
ToolType,
defineSingleFileTool,
defineMultiFileTool,
defineCustomTool,
};
export type {
ToolOperationConfig,
ToolOperationHook,
@@ -69,10 +77,10 @@ export { createStandardErrorHandler } from "@app/utils/toolErrorHandler";
* Shared hook for tool operations providing consistent error handling, progress tracking,
* and FileContext integration. Eliminates boilerplate while maintaining flexibility.
*
* Supports three tool patterns:
* 1. Single-file tools: Set multiFileEndpoint: false, processes files individually
* 2. Multi-file tools: Set multiFileEndpoint: true, single API call with all files
* 3. Complex tools: Provide customProcessor for full control over processing logic
* Supports three tool patterns, selected by the config's toolType:
* 1. Single-file tools (ToolType.singleFile): processes files individually
* 2. Multi-file tools (ToolType.multiFile): single API call with all files
* 3. Complex tools (ToolType.custom): customProcessor takes full control
*
* @param config - Tool operation configuration
* @returns Hook interface with state and execution methods
@@ -2,7 +2,7 @@ import { useTranslation } from "react-i18next";
import {
useToolOperation,
ToolOperationHook,
ToolType,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import {
SignParameters,
@@ -50,8 +50,7 @@ export const buildSignFormData = (
};
// Static configuration object
export const signOperationConfig = {
toolType: ToolType.singleFile,
export const signOperationConfig = defineSingleFileTool({
buildFormData: buildSignFormData,
operationType: "sign",
// Signing is applied client-side in the viewer (see createStampTool ->
@@ -60,7 +59,7 @@ export const signOperationConfig = {
endpoint: null,
filePrefix: "signed_",
defaultParameters: DEFAULT_PARAMETERS,
} as const;
});
export const useSignOperation = (): ToolOperationHook<SignParameters> => {
const { t } = useTranslation();
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import {
ToolType,
useToolOperation,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import {
fileOnlyMapping,
@@ -26,15 +26,14 @@ export const buildSingleLargePageFormData = (
): FormData => objectToFormData(toApiParams(), { fileInput: file });
// Static configuration object
export const singleLargePageOperationConfig = {
toolType: ToolType.singleFile,
export const singleLargePageOperationConfig = defineSingleFileTool({
buildFormData: buildSingleLargePageFormData,
toApiParams,
fromApiParams,
operationType: "pdfToSinglePage",
endpoint: ENDPOINT,
defaultParameters,
} as const;
});
export const useSingleLargePageOperation = () => {
const { t } = useTranslation();
@@ -1,9 +1,9 @@
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import {
ToolType,
useToolOperation,
ToolOperationConfig,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import {
objectToFormData,
@@ -33,7 +33,8 @@ const SPLIT_ENDPOINTS = {
[SPLIT_METHODS.BY_POSTER]: "/api/v1/general/split-for-poster-print",
} as const satisfies Record<SplitMethod, ToolEndpoint>;
type SplitApiParams = ToolApiParams[(typeof SPLIT_ENDPOINTS)[SplitMethod]];
type SplitEndpoint = (typeof SPLIT_ENDPOINTS)[SplitMethod];
type SplitApiParams = ToolApiParams[SplitEndpoint];
type SectionsApiParams =
ToolApiParams[(typeof SPLIT_ENDPOINTS)[typeof SPLIT_METHODS.BY_SECTIONS]];
type PosterApiParams =
@@ -165,20 +166,19 @@ export const buildSplitFormData = (
): FormData =>
objectToFormData(splitToApiParams(parameters), { fileInput: file });
export const getSplitEndpoint = (parameters: SplitParameters): ToolEndpoint =>
export const getSplitEndpoint = (parameters: SplitParameters): SplitEndpoint =>
// Default to BY_PAGES when no method is selected yet.
SPLIT_ENDPOINTS[parameters.method ?? SPLIT_METHODS.BY_PAGES];
// Static configuration object
export const splitOperationConfig = {
toolType: ToolType.singleFile,
export const splitOperationConfig = defineSingleFileTool({
buildFormData: buildSplitFormData,
toApiParams: splitToApiParams,
fromApiParams: splitFromApiParams,
operationType: "split",
endpoint: getSplitEndpoint,
defaultParameters,
} as const;
});
export const useSplitOperation = () => {
const { t } = useTranslation();
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import {
ToolType,
useToolOperation,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import {
objectToFormData,
@@ -35,16 +35,14 @@ export const buildTimestampPdfFormData = (
): FormData =>
objectToFormData(timestampPdfToApiParams(parameters), { fileInput: file });
export const timestampPdfOperationConfig = {
toolType: ToolType.singleFile,
export const timestampPdfOperationConfig = defineSingleFileTool({
buildFormData: buildTimestampPdfFormData,
toApiParams: timestampPdfToApiParams,
fromApiParams: timestampPdfFromApiParams,
operationType: "timestampPdf",
endpoint: ENDPOINT,
multiFileEndpoint: false,
defaultParameters,
} as const;
});
export const useTimestampPdfOperation = () => {
const { t } = useTranslation();
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import {
ToolType,
useToolOperation,
defineSingleFileTool,
} from "@app/hooks/tools/shared/useToolOperation";
import {
fileOnlyMapping,
@@ -26,15 +26,14 @@ export const buildUnlockPdfFormsFormData = (
): FormData => objectToFormData(toApiParams(), { fileInput: file });
// Static configuration object
export const unlockPdfFormsOperationConfig = {
toolType: ToolType.singleFile,
export const unlockPdfFormsOperationConfig = defineSingleFileTool({
buildFormData: buildUnlockPdfFormsFormData,
toApiParams,
fromApiParams,
operationType: "unlockPDFForms",
endpoint: ENDPOINT,
defaultParameters,
} as const;
});
export const useUnlockPdfFormsOperation = () => {
const { t } = useTranslation();
@@ -1,7 +1,6 @@
import apiClient from "@app/services/apiClient";
import {
ToolType,
CustomToolOperationConfig,
defineCustomTool,
CustomProcessorResult,
} from "@app/hooks/tools/shared/toolOperationTypes";
import {
@@ -104,10 +103,10 @@ const processPdfCommentAgent = async (
return { files: [resultFile] };
};
export const pdfCommentAgentOperationConfig = {
toolType: ToolType.custom,
operationType: "pdfCommentAgent",
endpoint: PDF_COMMENT_AGENT_ENDPOINT,
customProcessor: processPdfCommentAgent,
defaultParameters,
} as const satisfies CustomToolOperationConfig<PdfCommentAgentParameters>;
export const pdfCommentAgentOperationConfig =
defineCustomTool<PdfCommentAgentParameters>({
operationType: "pdfCommentAgent",
endpoint: PDF_COMMENT_AGENT_ENDPOINT,
customProcessor: processPdfCommentAgent,
defaultParameters,
});