Skip to content

Paths could be order correctly as a rule #678

@Samuel-Morgan-Tyghe

Description

@Samuel-Morgan-Tyghe

With MSW exact matching is not a thing and apparently some BE's aswell like express:
mswjs/msw#1426

So we end up having the below happen in our code generated tool:

In the contract we don't care what the order of the paths are. The problem with this is that the order matters for the msw handlers.

If I import all handlers and run a mocked version of the site. Then handlers could intercept incorrect paths.

example:
This handler is ordered first

export const getGetPermissionsForUserMockHandler = (overrideResponse?: UsersPermissionEntries | ((info: Parameters<Parameters<typeof http.get>[1]>[0]) => Promise<UsersPermissionEntries> | UsersPermissionEntries)) => {
  return http.get('*/api/authentication/v1/permissions/:userId', async (info) => {await delay(10);
  
    return new HttpResponse(JSON.stringify(overrideResponse !== undefined
    ? (typeof overrideResponse === "function" ? await overrideResponse(info) : overrideResponse)
    : getGetPermissionsForUserResponseMock()),
      { status: 200,
        headers: { 'Content-Type': 'application/json' }
      })
  })
}

This handler is ordered second

export const getGetAllPermissionsForCurrentUserMockHandler = (overrideResponse?: UsersPermissionEntries | ((info: Parameters<Parameters<typeof http.get>[1]>[0]) => Promise<UsersPermissionEntries> | UsersPermissionEntries)) => {
  return http.get('*/api/authentication/v1/permissions/all', async (info) => {await delay(10);
  
    return new HttpResponse(JSON.stringify(overrideResponse !== undefined
    ? (typeof overrideResponse === "function" ? await overrideResponse(info) : overrideResponse)
    : getGetAllPermissionsForCurrentUserResponseMock()),
      { status: 200,
        headers: { 'Content-Type': 'application/json' }
      })
  })
}

'permissions/:userId', is the same as 'permissions/*',
which is a problem when you have a separate endpoint for 'permissions/all'.

This could be checked on build and ordered correctly:

/**
 * Reorders an array of URL path strings to prevent conflicts in routing libraries like MSW.
 * It sorts paths from most specific to least specific.
 *
 * The sorting rules are:
 * 1. Static segments are more specific than dynamic segments (e.g., 'all' comes before ':id').
 * 2. Longer paths are generally more specific (e.g., '/users/all/details' comes before '/users/all').
 * 3. Wildcards ('*') are the least specific.
 *
 * @param {string[]} paths - An array of path strings.
 * @returns {string[]} A new array with the paths sorted correctly.
 */
function reorderPathsForMsw(paths) {
  // Create a copy to avoid modifying the original array.
  const pathsCopy = [...paths];

  const getSegmentSpecificity = (segment) => {
    if (segment.startsWith(':')) {
      return 1; // Dynamic parameter
    }
    if (segment === '*') {
      return 0; // Wildcard
    }
    return 2; // Static segment
  };

  pathsCopy.sort((pathA, pathB) => {
    const segmentsA = pathA.split('/');
    const segmentsB = pathB.split('/');
    const minLength = Math.min(segmentsA.length, segmentsB.length);

    // Compare segments one by one
    for (let i = 0; i < minLength; i++) {
      const specificityA = getSegmentSpecificity(segmentsA[i]);
      const specificityB = getSegmentSpecificity(segmentsB[i]);

      if (specificityA !== specificityB) {
        // Higher specificity value should come first (sort descending)
        return specificityB - specificityA;
      }
    }

    // If all common segments are equally specific, the longer path is more specific
    // (e.g., '/users/all/details' vs '/users/all'). Sort descending by length.
    return segmentsB.length - segmentsA.length;
  });

  return pathsCopy;
}

// --- Example Usage ---

const conflictingPaths = [
  '*/api/permissions/:userId', // Dynamic
  '/api/items/:itemId/details', // Dynamic but more specific
  '*/api/permissions/all', // Static
  '/api/items/latest', // Static
  '/api/items/:itemId', // Dynamic
  '*', // Wildcard
];

const orderedPaths = reorderPathsForMsw(conflictingPaths);

console.log("Original Paths:");
console.log(conflictingPaths);
/*
[
  "*/api/permissions/:userId",
  "/api/items/:itemId/details",
  "*/api/permissions/all",
  "/api/items/latest",
  "/api/items/:itemId",
  "*"
]
*/

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions