Building a Modern GraphQL Integration Between Payload CMS and Next.js: Part 2

4. Dezember 2024 von Moritz Thomas

Query Map Generation and Management

With our persisted query endpoint in place, we need a reliable way to generate and maintain our query map. This is handled by a dedicated script that processes GraphQL files and generates a TypeScript file containing our query mappings. Let's dive into how this works.

The Query Map Generator

The generator script takes GraphQL files from a specified source directory, processes them, and creates a TypeScript file containing both the queries and their mappings. Here's the core implementation:

import fs from "node:fs/promises";
import path from "node:path";
import crypto from "node:crypto";

function generateQueryKey(filename: string, content: string): string {
  const baseName = filename.replace(".graphql", "");
  const hash = crypto.createHash("sha256").update(content).digest("hex").slice(0, 8);
  return `${baseName}_${hash}`;
}

export async function generateGQLQueryMap(sourceDir: string, outputPath: string) {
  // First read existing queries if the file exists
  const existingQueries: Record<string, string> = {};
  let existingKeyMapping: Record<string, string> = {};
  const existingComments: Record<string, string> = {};

  try {
    const existingContent = await fs.readFile(outputPath, "utf8");

    // Extract the QUERIES object including comments
    const queriesSection = existingContent.match(
      /export const QUERIES: Record<string, string> = {([\S\s]*?)};/,
    );
    if (queriesSection) {
      // Parse the queries section while preserving comments
      const queriesEntries = queriesSection[1].trim().split(/,\n\n/);
      for (const entry of queriesEntries) {
        // Extract comment if exists
        const commentMatch = entry.match(/^\s*\/\/ Added\/Updated: .+\n/);
        const comment = commentMatch ? commentMatch[0] : "";

        // Extract key and value
        const match = entry.match(/"([^"]+)":\s*"([^"]+)"/);
        if (match) {
          existingQueries[match[1]] = match[2];
          if (comment) {
            existingComments[match[1]] = comment;
          }
        }
      }
    }
  } catch {
    // File doesn't exist yet, start fresh
    console.log("Starting fresh - no existing file found");
  }

The script has several important features:

1. Query Key Generation:

function generateQueryKey(filename: string, content: string): string {
  const baseName = filename.replace(".graphql", "");
  const hash = crypto.createHash("sha256").update(content).digest("hex").slice(0, 8);
  return `${baseName}_${hash}`;
}

Each query gets a unique identifier composed of its filename and a content hash. This ensures that different versions of the same query can coexist, which is crucial for zero-downtime deployments.

2. Preserving Existing Queries: The script carefully preserves existing queries while adding new ones. This is essential for maintaining backward compatibility during deployments. Old query versions remain available even after new versions are added, allowing different versions of the frontend to continue functioning.

3. Timestamp Comments: Every query entry includes a timestamp comment indicating when it was added or updated. This provides an audit trail and helps with future cleanup of truly obsolete queries:

// Added/Updated: 2024-01-15
"uiStrings_a1b2c3d4": "query uiStrings..."

Integration with Development Workflow

The query map generator is integrated into our development workflow through a custom CLI tool:

program
  .command("gql:query-map")
  .description("Generate GraphQL query map and copy to Payload CMS")
  .action(async () => {
    try {
      // First generate the query map
      await generateGQLQueryMap(
        "./lib/fetch", // source directory with .graphql files
        "./lib/generated/gql-query-map.ts", // output path
      );

      // Now copy to Payload CMS repository
      const sourceFile = path.resolve("./lib/generated/gql-query-map.ts");
      const targetFile = path.resolve("../payload/src/generated/gql-query-map.ts");

      // Ensure target directory exists
      await fs.mkdir(path.dirname(targetFile), { recursive: true });

      // Copy the file
      await fs.copyFile(sourceFile, targetFile);

      console.log("✓ Generated and copied query map to Payload CMS");
    } catch (error) {
      console.error("Error generating/copying query map:", error);
      process.exit(1);
    }
  });

This CLI command handles both generating the query map and copying it to the Payload CMS project. This automation ensures that our frontend and backend stay in sync.

Deployment Considerations

The query map system is designed with safe deployments in mind. Here's how it handles various deployment scenarios:

  1. Adding New Queries: When a new query is added, it gets a fresh query key. Existing queries remain unchanged, ensuring that live clients continue to function.

  2. Modifying Queries: When a query is modified, a new version is created with an updated hash. The old version remains in the map, allowing existing clients to continue functioning while new clients use the updated version.

  3. Query Cleanup: While the query map can grow over time as new versions are added, the actual storage overhead is minimal. A typical project might use 8.5KB after months of development. The timestamp comments make it possible to implement cleanup of old queries if needed, though this is rarely necessary in practice.

This approach to query management provides a robust foundation for our persisted query system.

Kommentare

Noch keine Kommentare, sei der Erste:

© 2024 von Moritz Thomas