UNPKG

@newmo/graphql-fake-server

Version:

GraphQL fake server for testing

880 lines 35.2 kB
import fs from "node:fs/promises"; import http from "node:http"; import { isDeepStrictEqual } from "node:util"; import { ApolloServer } from "@apollo/server"; import { ApolloServerPluginDrainHttpServer } from "@apollo/server/plugin/drainHttpServer"; import { expressMiddleware } from "@as-integrations/express5"; import { addMocksToSchema } from "@graphql-tools/mock"; import { makeExecutableSchema } from "@graphql-tools/schema"; import { serve } from "@hono/node-server"; import { createMock } from "@newmo/graphql-fake-core"; import corsExpress from "cors"; import express from "express"; import { buildSchema } from "graphql/utilities/index.js"; // @ts-expect-error -- no types import depthLimit from "graphql-depth-limit"; import { Hono } from "hono"; import { cors } from "hono/cors"; import { proxy } from "hono/proxy"; import { createLogger } from "./logger.js"; // @ts-expect-error -- biome error const ENV_HOSTNAME = process.env.HOSTNAME || "0.0.0.0"; /** * Custom startStandaloneServer with CORS configuration * This restricts CORS to only allow localhost, internal network connections, and specified allowed origins */ const startStandaloneServerWithCORS = async (server, options, allowedCORSOrigins = []) => { // Create Express app with custom CORS configuration const app = express(); const httpServer = http.createServer(app); // Add drain plugin for graceful shutdown server.addPlugin(ApolloServerPluginDrainHttpServer({ httpServer })); // Ensure server is started await server.start(); // Set up Express middleware with strict CORS that only allows localhost app.use("/", corsExpress({ origin: (origin, callback) => { // Allow requests with no origin (like mobile apps, curl, etc) if (!origin) return callback(null, true); // Allow localhost, loopback addresses, and explicitly allowed origins if (isLocalRequest(origin)) { return callback(null, true); } // Allow explicitly allowed origins from configuration if (allowedCORSOrigins.includes(origin)) { return callback(null, true); } // Deny all other origins return callback(new Error("Not allowed by CORS"), false); }, methods: ["POST", "GET", "OPTIONS"], credentials: false, }), express.json({ limit: "50mb" }), // @ts-expect-error -- express 5 types are not compatible with apollo-server expressMiddleware(server, options)); // Start the server const port = options.listen.port ?? 4000; await new Promise((resolve) => httpServer.listen({ port }, resolve)); return { url: `http://${ENV_HOSTNAME}:${port}`, httpServer, }; }; const creteApolloServer = async (options) => { const mocks = Object.fromEntries(Object.entries(options.mockObject).map(([key, value]) => { return [key, () => value]; })); return new ApolloServer({ schema: addMocksToSchema({ schema: makeExecutableSchema({ typeDefs: options.schema, }), mocks, }), validationRules: [depthLimit(options.maxQueryDepth)], }); }; // Allowed condition types const ALLOWED_CONDITION_TYPES = ["count", "variables"]; /** * Check if two condition types are conflicting and return specific error message * Only the following combinations are allowed: * - count + count * - variables + variables * - variables + no condition (undefined) * - no condition (undefined) + no condition (undefined) * All other combinations are conflicting */ const areConditionTypesConflicting = (conditionType1, conditionType2) => { // Define allowed combinations with their descriptions const allowedCombinations = new Map([ // Multiple count conditions for the same operation (e.g., 1st call, 2nd call) ["count,count", "Multiple count-based conditions are allowed for different call counts"], // Multiple variables conditions for the same operation (e.g., different variable sets) [ "variables,variables", "Multiple variable-based conditions are allowed for different variable sets", ], // Variables condition can coexist with default fallback ["variables,undefined", "Variable-based condition can coexist with default fallback"], // Default fallback can coexist with variables condition ["undefined,variables", "Default fallback can coexist with variable-based condition"], // Multiple default conditions - overwrite with the last one ["undefined,undefined", "Multiple default conditions are allowed (latest will be used)"], ]); const combinationKey = `${conditionType1 ?? "undefined"},${conditionType2 ?? "undefined"}`; // If the combination is allowed, return no conflict if (allowedCombinations.has(combinationKey)) { return { isConflicting: false }; } // Generate specific error message for conflicting combinations const getTypeDescription = (type) => { switch (type) { case "count": return "count-based condition (e.g., { type: 'count', value: 1 })"; case "variables": return "variables-based condition (e.g., { type: 'variables', value: {...} })"; case undefined: return "default condition (no requestCondition specified)"; default: return `unknown condition type: ${type}`; } }; const type1Desc = getTypeDescription(conditionType1); const type2Desc = getTypeDescription(conditionType2); // Specific error messages for common problematic combinations if ((conditionType1 === "count" && conditionType2 === "variables") || (conditionType1 === "variables" && conditionType2 === "count")) { const errorMessage = "Cannot mix count-based and variables-based conditions for the same operation. " + "Use either multiple count conditions (for different call numbers) or multiple variables conditions (for different variable sets), " + `but not both. Current conflict: ${type1Desc} vs ${type2Desc}`; return { isConflicting: true, errorMessage }; } const errorMessage = `Conflicting condition types detected: ${type1Desc} vs ${type2Desc}. ` + "Allowed combinations are: count+count, variables+variables, variables+default, or default+default."; return { isConflicting: true, errorMessage }; }; /** * Get condition type from a RegisterSequenceOptions */ const getConditionType = (fake) => { return fake.requestCondition?.type; }; /** * Check for condition conflicts in existing fakes for the same operation */ const checkConditionConflicts = (newFake, existingConditionalFakes, existingDefaultFake) => { const errors = []; const newConditionType = getConditionType(newFake); // Check conflicts with existing conditional fakes for (const existingFake of existingConditionalFakes) { const existingConditionType = getConditionType(existingFake); const conflictResult = areConditionTypesConflicting(newConditionType, existingConditionType); if (conflictResult.isConflicting && conflictResult.errorMessage) { errors.push(conflictResult.errorMessage); } } // Check conflicts with existing default fake (no condition) if (existingDefaultFake) { const existingConditionType = getConditionType(existingDefaultFake); const conflictResult = areConditionTypesConflicting(newConditionType, existingConditionType); if (conflictResult.isConflicting && conflictResult.errorMessage) { errors.push(conflictResult.errorMessage); } } return errors; }; /** * Validate condition rule structure */ const validateConditionRule = (condition) => { if (typeof condition !== "object" || condition === null) { return { ok: false, error: "Condition must be an object" }; } if (!("type" in condition) || typeof condition.type !== "string") { return { ok: false, error: "Condition must have a 'type' field of type string", }; } // Check if type is in the allow list if (!ALLOWED_CONDITION_TYPES.includes(condition.type)) { return { ok: false, error: `Unknown condition type '${condition.type}'. Allowed types: ${ALLOWED_CONDITION_TYPES.join(", ")}`, }; } if (!("value" in condition)) { return { ok: false, error: "Condition must have a 'value' field" }; } switch (condition.type) { case "count": if (typeof condition.value !== "number") { return { ok: false, error: "Count condition value must be a number" }; } if (condition.value <= 0) { return { ok: false, error: "Count condition value must be greater than 0", }; } return { ok: true, data: condition }; case "variables": if (typeof condition.value !== "object" || condition.value === null) { return { ok: false, error: "Variables condition value must be an object", }; } if (Array.isArray(condition.value)) { return { ok: false, error: "Variables condition value must be an object, not an array", }; } return { ok: true, data: condition }; default: return { ok: false, error: `Unsupported condition type '${condition.type}'`, }; } }; const validateSequenceRegistration = (data) => { if (typeof data !== "object" || data === null) { return { ok: false, error: "Request body must be an object" }; } // Validate request condition if ("requestCondition" in data && data.requestCondition !== undefined) { const conditionResult = validateConditionRule(data.requestCondition); if (!conditionResult.ok) { return { ok: false, error: `Invalid request condition: ${conditionResult.error}`, }; } } if (!("type" in data) || typeof data.type !== "string") { return { ok: false, error: "Request body must have a 'type' field of type string", }; } if (!("operationName" in data) || typeof data.operationName !== "string") { return { ok: false, error: "Request body must have an 'operationName' field of type string", }; } if (data.type === "network-error") { if (!("errors" in data) || !Array.isArray(data.errors)) { return { ok: false, error: "Network error type must have an 'errors' field of type array", }; } if (!("responseStatusCode" in data) || typeof data.responseStatusCode !== "number") { return { ok: false, error: "Network error type must have a 'responseStatusCode' field of type number", }; } return { ok: true, data: data }; } if (data.type === "operation") { if (!("data" in data) || typeof data.data !== "object" || data.data === null) { return { ok: false, error: "Operation type must have a 'data' field of type object", }; } return { ok: true, data: data }; } return { ok: false, error: `Unknown request type '${data.type}'. Allowed types: 'operation', 'network-error'`, }; }; class LRUMap { map = new Map(); keys = []; maxSize; constructor({ maxSize }) { this.maxSize = maxSize; } set(key, value) { this.map.set(key, value); this.keys.push(key); if (this.keys.length > this.maxSize) { const oldestKey = this.keys.shift(); if (oldestKey) { this.map.delete(oldestKey); } } } get(key) { return this.map.get(key); } } // Map key is sequenceId x operationName // allow to register multiple operations with the same sequenceId at the same time // However, sequenceId x operationName must be unique // If the same sequenceId x operationName is registered, the previous one is overwritten const createMapKey = ({ sequenceId, operationName, }) => { return `${sequenceId}.${operationName}`; }; // Private IP address ranges defined in RFC 1918 // See: https://www.rfc-editor.org/rfc/rfc1918 const privateIPRanges = [ /^192\.168\.\d{1,3}\.\d{1,3}$/, // 192.168.0.0/16 /^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, // 10.0.0.0/8 /^172\.(1[6-9]|2\d|3[0-1])\.\d{1,3}\.\d{1,3}$/, // 172.16.0.0/12 ]; /** * Check if the origin is a local address * @param origin */ const isLocalRequest = (origin) => { if (!origin) return false; try { const url = new URL(origin); const hostname = url.hostname; // localhost and 127.0.0.1 are standard local addresses if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === ENV_HOSTNAME) { return true; } return privateIPRanges.some((range) => range.test(hostname)); } catch { return false; } }; const createRoutingServer = async ({ logLevel, ports, maxRegisteredSequences, allowedCORSOrigins, }) => { const logger = createLogger(logLevel); const app = new Hono(); // pass through to apollo server const passToApollo = async (c) => { logger.debug("passToApollo: starting"); // remove prefix // prefix = /app1/*, path = /app1/a/b // => suffix_path = /a/b // let path = new URL(c.req.raw.url).pathname let path = c.req.path; logger.debug("passToApollo: got path", { path, routePath: c.req.routePath, }); path = path.replace(new RegExp(`^${c.req.routePath.replace("*", "")}`), "/"); let url = `http://${ENV_HOSTNAME}:${ports.apolloServer}${path}`; // add params to URL if (c.req.query()) url = `${url}?${new URLSearchParams(c.req.query())}`; logger.debug("passToApollo: built URL", { url }); const sequenceId = c.req.header("sequence-id"); logger.debug("passToApollo: getting request body", { sequenceId }); const requestBody = await c.req.raw.clone().json(); logger.debug("passToApollo: got request body", { requestBody }); const operationName = typeof requestBody === "object" && requestBody !== null && "operationName" in requestBody ? requestBody.operationName : undefined; // request logger.debug("passToApollo: calling proxy", { url, sequenceId, operationName, headers: c.req.header(), }); const proxyResponse = await proxy(url, { raw: c.req.raw, headers: { ...c.req.header(), }, }); logger.debug("passToApollo: proxy call completed", { sequenceId, operationName, status: proxyResponse.status, headers: Object.fromEntries(proxyResponse.headers), }); // log response with pipe if (proxyResponse.status === 101) return proxyResponse; // save request and response for /called api if (sequenceId && typeof operationName === "string") { logger.debug("passToApollo: getting response body for caching"); const responseBody = (await proxyResponse.clone().json()); logger.debug("passToApollo: parsed response body", { responseBody, }); const cacheKey = createMapKey({ sequenceId, operationName, }); logger.debug("save called result", { sequenceId, operationName, cacheKey, }); sequenceCalledResultLruMap.set(cacheKey, [ ...(sequenceCalledResultLruMap.get(cacheKey) ?? []), { requestTimestamp: Date.now(), request: { headers: Object.fromEntries(c.req.raw.headers), body: requestBody, }, response: { status: proxyResponse.status, headers: Object.fromEntries(proxyResponse.headers), body: responseBody, }, }, ]); } logger.debug("passToApollo: returning proxy response", { sequenceId, operationName, status: proxyResponse.status, }); return proxyResponse; }; // sequenceId x operationName -> FakeResponse const sequenceFakeResponseLruMap = new LRUMap({ maxSize: maxRegisteredSequences, }); // Manage conditional fake responses (store multiple conditional responses) const conditionalFakeResponseMap = new LRUMap({ maxSize: maxRegisteredSequences, }); // Track call count const callCountMap = new LRUMap({ maxSize: maxRegisteredSequences, }); // sequenceId x operationName -> Called Result // CalledResult is first request is index 0, second request is index 1 and so on const sequenceCalledResultLruMap = new LRUMap({ maxSize: maxRegisteredSequences, }); // /fake api does not support CORS // because it allows any user to modify the response // If you need to support CORS, implement with checking the origin or something app.post("/fake", async (c) => { logger.debug("/fake"); const sequenceId = c.req.header("sequence-id"); if (!sequenceId) { return Response.json({ ok: false, errors: ["sequence-id is required"], }, { status: 400, }); } const body = await c.req.json(); logger.debug("/fake: got fake body", { sequenceId, body, }); const validationResult = validateSequenceRegistration(body); if (!validationResult.ok) { return Response.json({ ok: false, errors: [validationResult.error] }, { status: 400, }); } const operationName = validationResult.data.operationName; logger.debug("/fake got body type", { sequenceId, type: validationResult.data.type, requestCondition: validationResult.data.requestCondition, }); const baseKey = createMapKey({ sequenceId, operationName, }); // Check for condition conflicts before registration const existingConditionalFakes = conditionalFakeResponseMap.get(baseKey) || []; const existingDefaultFake = sequenceFakeResponseLruMap.get(baseKey); const conflictErrors = checkConditionConflicts(validationResult.data, existingConditionalFakes, existingDefaultFake); if (conflictErrors.length > 0) { return Response.json({ ok: false, errors: conflictErrors }, { status: 400, }); } // Register as conditional fake if request condition exists if (validationResult.data.requestCondition) { const existingConditionalFakes = conditionalFakeResponseMap.get(baseKey) || []; // Overwrite if same condition exists, otherwise add new const existingIndex = existingConditionalFakes.findIndex((fake) => fake.requestCondition && JSON.stringify(fake.requestCondition) === JSON.stringify(validationResult.data.requestCondition)); if (existingIndex >= 0) { existingConditionalFakes[existingIndex] = validationResult.data; } else { existingConditionalFakes.push(validationResult.data); } // Sort by condition specificity (evaluate more specific conditions first) existingConditionalFakes.sort((a, b) => { const scoreA = a.requestCondition ? calculateConditionSpecificity(a.requestCondition) : 0; const scoreB = b.requestCondition ? calculateConditionSpecificity(b.requestCondition) : 0; return scoreB - scoreA; // Descending order }); conditionalFakeResponseMap.set(baseKey, existingConditionalFakes); } else { // Without condition, use traditional approach sequenceFakeResponseLruMap.set(baseKey, validationResult.data); } return Response.json({ ok: true }, { status: 200, }); }); app.use("/fake/called", async (c) => { // Return CalledResult matching sequenceId x operationName const sequenceId = c.req.header("sequence-id"); if (!sequenceId) { return Response.json({ ok: false, errors: ["sequence-id is required"], }, { status: 400, }); } // Get operationName from req.body const body = await c.req.json(); const operationName = body.operationName; if (!operationName) { return Response.json({ ok: false, errors: ["operationName is required"], }, { status: 400, }); } const key = createMapKey({ sequenceId, operationName, }); // if not found, return empty array const result = sequenceCalledResultLruMap.get(key); if (!result) { return Response.json({ ok: true, data: [] }, { status: 200, }); } return Response.json({ ok: true, data: result }, { status: 200, }); }); const fakeGraphQLQuery = async (c) => { logger.debug("fakeGraphQLQuery: starting"); const _requestTimestamp = Date.now(); /** * Steps: * 1. Receive a request for a GraphQL query * 2. Does it contain a sequence id? * - if Yes: type is network error → return an error * - if No: Pass through to Apollo Server -> exit * 3. Send a request to Apollo Server * 4. Merge the registration data with the response from 3 * 5. Return the merged data */ const sequenceId = c.req.header("sequence-id"); logger.debug("fakeGraphQLQuery: getting request body", { sequenceId }); const requestBody = await c.req.raw.clone().json(); logger.debug("fakeGraphQLQuery: got request body", { requestBody, }); const requestOperationName = typeof requestBody === "object" && requestBody !== null && "operationName" in requestBody && requestBody.operationName && typeof requestBody.operationName === "string" ? requestBody.operationName : undefined; logger.debug(`fakeGraphQLQuery: operationName: ${requestOperationName} sequenceId: ${sequenceId}`, { sequenceId, }); // 2. Does it contain a sequence id? if (!sequenceId) { logger.debug("fakeGraphQLQuery: no sequenceId, passing to Apollo"); return passToApollo(c); } if (!requestOperationName) { logger.debug("fakeGraphQLQuery: no operationName, passing to Apollo"); return passToApollo(c); } const baseKey = createMapKey({ sequenceId, operationName: requestOperationName, }); // Increment call count const currentCallCount = (callCountMap.get(baseKey) || 0) + 1; callCountMap.set(baseKey, currentCallCount); // Get request variables const requestVariables = typeof requestBody === "object" && requestBody !== null && "variables" in requestBody && typeof requestBody.variables === "object" && requestBody.variables !== null ? requestBody.variables : undefined; // Check conditional fakes first const conditionalFakes = conditionalFakeResponseMap.get(baseKey); // Find the first matching conditional fake based on call count and variables // If no conditional fake matches, use the default fake from sequenceFakeResponseLruMap const matchedFake = findMatchedConditionalFake({ conditionalFakes: conditionalFakes, currentCallCount: currentCallCount, requestVariables: requestVariables, logger: logger, sequenceId: sequenceId, requestOperationName: requestOperationName, }) ?? sequenceFakeResponseLruMap.get(baseKey); logger.debug(`fakeGraphQLQuery: sequence-id: ${sequenceId} x operationName: ${requestOperationName}, fake exists: ${Boolean(matchedFake)}`, { matchedFake, sequenceId, operationName: requestOperationName, callCount: currentCallCount, }); if (!matchedFake) { logger.debug("fakeGraphQLQuery: no fake found, passing to Apollo"); return passToApollo(c); } if (requestOperationName !== matchedFake.operationName) { logger.debug("fakeGraphQLQuery: operationName mismatch, returning error"); return Response.json({ errors: [ `operationName does not match. operationName: ${requestOperationName} sequenceId: ${sequenceId}`, ], }, { status: 400, }); } if (matchedFake.type === "network-error") { logger.debug("fakeGraphQLQuery: network-error type, returning error"); // Record call history for error responses as well const cacheKey = createMapKey({ sequenceId, operationName: requestOperationName, }); sequenceCalledResultLruMap.set(cacheKey, [ ...(sequenceCalledResultLruMap.get(cacheKey) ?? []), { requestTimestamp: Date.now(), request: { headers: Object.fromEntries(c.req.raw.headers), body: requestBody, }, response: { status: matchedFake.responseStatusCode, headers: { "Content-Type": "application/json" }, body: { errors: matchedFake.errors, }, }, }, ]); return new Response(JSON.stringify({ errors: matchedFake.errors, }), { status: matchedFake.responseStatusCode, }); } // 3. Send a request to Apollo Server logger.debug("fakeGraphQLQuery: sending request to apollo server", { sequenceId, }); const proxyResponse = await proxy(`http://${ENV_HOSTNAME}:${ports.apolloServer}/graphql`, { raw: c.req.raw, headers: { ...c.req.header(), }, }); logger.debug("fakeGraphQLQuery: apollo server response completed", { sequenceId, status: proxyResponse.status, headers: Object.fromEntries(proxyResponse.headers), }); if (proxyResponse.status === 101) return proxyResponse; // 4. Get response body logger.debug("fakeGraphQLQuery: getting response body"); const responseBody = (await proxyResponse.json()); logger.debug("fakeGraphQLQuery: parsed response body", { responseBody, }); // 5. Merge the registration data with the response const data = matchedFake.data; logger.debug(`fakeGraphQLQuery: starting data merge sequence-id: ${sequenceId}`, { data, responseBody, }); // Use bracket notation for properties from index signature const responseData = responseBody["data"]; const merged = { ...(typeof responseData === "object" && responseData !== null ? responseData : {}), ...data, }; const cacheKey = createMapKey({ sequenceId, operationName: requestOperationName, }); sequenceCalledResultLruMap.set(cacheKey, [ ...(sequenceCalledResultLruMap.get(cacheKey) ?? []), { requestTimestamp: Date.now(), request: { headers: Object.fromEntries(c.req.raw.headers), body: requestBody, }, response: { status: proxyResponse.status, headers: Object.fromEntries(proxyResponse.headers), body: { data: merged, }, }, }, ]); logger.debug("fakeGraphQLQuery: merge completed, returning response"); // Let the server automatically calculate Content-Length to avoid issues with multi-byte characters const responseJson = JSON.stringify({ data: merged }); return new Response(responseJson, { status: proxyResponse.status, headers: { "Content-Type": "application/json", }, }); }; // graphql api is for browser and need to support CORS app.use("/graphql", cors({ origin: (origin) => { if (isLocalRequest(origin)) { return origin; } if (origin && allowedCORSOrigins.includes(origin)) { return origin; } return null; }, })); app.use("/query", cors({ origin: (origin) => { if (isLocalRequest(origin)) { return origin; } if (origin && allowedCORSOrigins.includes(origin)) { return origin; } return null; }, })); app.use("/graphql", fakeGraphQLQuery); app.use("/query", fakeGraphQLQuery); app.all("*", passToApollo); return app; }; export const createFakeServer = async (options) => { const { logLevel, maxFieldRecursionDepth, maxQueryDepth, maxRegisteredSequences, ports, schemaFilePath, defaultValues, allowedCORSOrigins, } = options; const logger = createLogger(logLevel); const schema = buildSchema(await fs.readFile(schemaFilePath, "utf-8")); const mockResult = await createMock({ schema, maxFieldRecursionDepth, defaultValues, }); if (!mockResult.ok) { logger.error("Failed to create mock data", mockResult); throw new Error("Failed to create mock data", { cause: mockResult.error, }); } logger.debug("created mock code", mockResult.code); logger.debug("created mock data", mockResult.mock); return createFakeServerInternal({ ports, schema, mockObject: mockResult.mock, maxQueryDepth, maxFieldRecursionDepth, maxRegisteredSequences, logLevel: logLevel ?? "info", allowedCORSOrigins, }); }; export const createFakeServerInternal = async (options) => { const apolloServer = await creteApolloServer(options); const routingServer = await createRoutingServer({ logLevel: options.logLevel, ports: options.ports, maxRegisteredSequences: options.maxRegisteredSequences, allowedCORSOrigins: options.allowedCORSOrigins, }); let routerServer = null; return { start: async () => { // Replace startStandaloneServer with our custom implementation await startStandaloneServerWithCORS(apolloServer, { listen: { port: options.ports.apolloServer }, }, options.allowedCORSOrigins); routerServer = serve({ fetch: routingServer.fetch, port: options.ports.fakeServer, }); return { urls: { fakeServer: `http://${ENV_HOSTNAME}:${options.ports.fakeServer}`, apolloServer: `http://${ENV_HOSTNAME}:${options.ports.apolloServer}`, }, }; }, stop: () => { apolloServer.stop(); routerServer?.close(); }, }; }; /** * Check if condition rule matches the current request context */ const evaluateCondition = (condition, context) => { switch (condition.type) { case "count": return context.callCount === condition.value; case "variables": if (!context.variables) return false; return isDeepStrictEqual(context.variables, condition.value); default: return false; } }; /** * Calculate condition specificity score (used for matching priority) */ const calculateConditionSpecificity = (condition) => { switch (condition.type) { case "count": return 10; // count conditions have medium priority case "variables": return 20; // variables conditions have high priority default: return 0; } }; /** * Find a matching conditional fake based on the current call count and request variables */ const findMatchedConditionalFake = ({ conditionalFakes, currentCallCount, requestVariables, logger, sequenceId, requestOperationName, }) => { if (conditionalFakes && conditionalFakes.length > 0) { // Find matching fake (already sorted by specificity in descending order) for (const fake of conditionalFakes) { if (fake.requestCondition) { const context = { callCount: currentCallCount, ...(requestVariables && { variables: requestVariables }), }; if (evaluateCondition(fake.requestCondition, context)) { logger.debug("fakeGraphQLQuery: matched conditional fake", { sequenceId, operationName: requestOperationName, requestCondition: fake.requestCondition, callCount: currentCallCount, variables: requestVariables, }); return fake; } } } } return undefined; }; //# sourceMappingURL=server.js.map