All files / src/api emptyApi.ts

83.33% Statements 25/30
86.95% Branches 20/23
100% Functions 3/3
83.33% Lines 25/30

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188                                              103x           103x                 12993x                   1327x 1327x             12993x 12993x 12993x 12993x       12993x 12993x 12993x       12993x         12993x                 1x                                                     12993x     12993x               12993x               12993x                                                           12993x                   696x               12868x       103x     255514x 103x    
// Template API used by the generated RTK Query client. The generated code imports
// `emptySplitApi`, while this module centralises the shared base URL and the
// flowsheet query/body injection used throughout the app.
// see also (openapi-config.json)
// and https://redux-toolkit.js.org/rtk-query/usage/code-generation#usage
 
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
import { toast } from "sonner";
import {
  getCachedFlowsheetAccess,
  isReadOnlyMutationBlocked,
} from "@/lib/flowsheetAccess";
import { baseUrl, resolveApiUrl } from "./apiBaseUrl";
import {
  canonicalizeFlowsheetQueryParam,
  getCurrentRevisionRouteScope,
  getFlowsheetIdFromUrl,
  isHistoricalRevisionMutation,
  isRevisionRestoreRequest,
  scopeFlowsheetRequestUrl,
  serializeQueryArgsWithRevision,
} from "./flowsheetRevisionScope";
 
const defaultBaseQuery = fetchBaseQuery({
  baseUrl,
  credentials: "include",
});
 
// Don't include 'flowsheet' for project endpoint bodies
export const shouldInjectFlowsheetIntoBody = ({
  flowsheetId,
  isJson,
  pathname,
}: {
  flowsheetId: number | null;
  isJson: boolean;
  pathname: string;
}) =>
  Boolean(flowsheetId && isJson && !pathname.startsWith("/api/core/projects/"));
 
/** Detect the backend's platform-owned history metadata on a mutation result. */
export const isRecordedFlowsheetMutation = ({
  data,
  headers,
}: {
  data: unknown;
  headers?: Headers;
}) => {
  const responseData = data as { operation_id?: number | string } | undefined;
  return Boolean(
    responseData?.operation_id !== undefined ||
      headers?.get("X-Flowsheet-Edit-Operation"),
  );
};
 
const customBaseQuery = async (args, api, extraOptions) => {
  const url = document.URL;
  const flowsheetId = getFlowsheetIdFromUrl(url);
  const reducerPath = emptySplitApi.reducerPath;
  const originalUrl = typeof args === "string" ? args : args.url;
  // RTK Query passes either a bare URL string for simple GETs or a request
  // object with explicit method/body fields for richer requests.
  const method =
    typeof args === "string" ? "GET" : (args.method?.toUpperCase() ?? "GET");
  const revisionScope = getCurrentRevisionRouteScope();
  const canonicalRequest = canonicalizeFlowsheetQueryParam(
    new URL(resolveApiUrl(originalUrl)),
    typeof args === "string" ? undefined : args.params,
  );
  const argsUrl = scopeFlowsheetRequestUrl({
    url: canonicalRequest.url,
    flowsheetId,
    revisionScope,
  });
  const restoresCurrentFromRevision = isRevisionRestoreRequest(
    method,
    argsUrl.pathname,
  );
 
  // Restore is an explicit mutation of Current selected from preview. Keep the
  // target state in the stable endpoint path and do not send historical read
  // scope, which the backend correctly rejects for every write.
  if (restoresCurrentFromRevision) {
    argsUrl.searchParams.delete("revision");
  }
 
  if (
    flowsheetId !== null &&
    isHistoricalRevisionMutation(method, revisionScope) &&
    !restoresCurrentFromRevision
  ) {
    toast.warning("This version is read-only", {
      description: "Return to current to edit or solve this flowsheet.",
    });
 
    return {
      error: {
        status: 403,
        data: {
          detail: "Historical versions are read-only.",
        },
      },
    };
  }
 
  if (typeof args === "string") {
    args = argsUrl.toString();
  } else if (typeof args === "object") {
    // Inject flowsheet into body (for POST, PUT, etc.)
    const isJson =
      args.body &&
      typeof args.body === "object" &&
      !(args.body instanceof FormData);
    const newBody = shouldInjectFlowsheetIntoBody({
      flowsheetId,
      isJson,
      pathname: argsUrl.pathname,
    })
      ? { ...args.body, flowsheet: flowsheetId }
      : args.body;
 
    args = {
      ...args,
      url: argsUrl.toString(),
      body: newBody,
      params: canonicalRequest.params,
    };
  }
 
  const access = getCachedFlowsheetAccess(
    api.getState() as Record<string, unknown>,
    reducerPath,
    flowsheetId,
  );
 
  if (
    isReadOnlyMutationBlocked({
      method,
      pathname: argsUrl.pathname,
      access,
    })
  ) {
    // Return a synthetic 403 before the request leaves the browser so every
    // mutation endpoint gets consistent read-only behaviour, even if the UI
    // surface forgot to disable a control.
    toast.warning("This flowsheet is read-only", {
      description: "Make a copy to edit or solve this flowsheet.",
    });
 
    return {
      error: {
        status: 403,
        data: {
          error: "This flowsheet is shared with read-only access.",
        },
      },
    };
  }
 
  const result = await defaultBaseQuery(args, api, extraOptions);
  if (method !== "GET" && !("error" in result)) {
    if (
      isRecordedFlowsheetMutation({
        data: result.data,
        headers: result.meta?.response?.headers,
      })
    ) {
      // Recorded mutations publish one common content identity. Feature code
      // does not maintain endpoint-specific undo/redo invalidation lists.
      api.dispatch(
        emptySplitApi.util.invalidateTags([
          "EditOperations",
          "FlowsheetContent",
        ]),
      );
    }
  }
  return result;
};
 
// initialize an empty api service that we'll inject endpoints into later as needed
export const emptySplitApi = createApi({
  baseQuery: customBaseQuery,
  tagTypes: ["EditOperations", "FlowsheetContent"],
  serializeQueryArgs: (query) => serializeQueryArgsWithRevision(query),
  endpoints: () => ({}),
});