Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[api-codegen-preset] How to generate types for both Admin and Storefront #1058

Closed
andywpt opened this issue Jun 17, 2024 · 3 comments
Closed

Comments

@andywpt
Copy link

andywpt commented Jun 17, 2024

Overview

It seems like I can only generate either Storefront types or Admin types in a single graphql-codegen command. Is it possible to generate both types in a single command ? Thanks.

.graphqlrc.ts

// For generating Storefront types
export default {
  schema: "https://shopify.dev/storefront-graphql-direct-proxy",
  documents: ["./src/**/*.{js,ts}"],
  projects: {
    default: shopifyApiProject({
      apiType: ApiType.Storefront,
      apiVersion: Config.Shopify.storefrontApiVersion,
      documents: ["./src/types/shopify/storefront/definitions/**/*.{js,ts}"],
      outputDir: "./src/types/shopify/storefront/generated-types",
      declarations: false
    })
  }
}

// For generating Admin types
export default {
  schema: "https://shopify.dev/admin-graphql-direct-proxy",
  documents: ["./src/**/*.{js,ts}"],
  projects: {
    default: shopifyApiProject({
      apiType: ApiType.Admin,
      apiVersion: Config.Shopify.adminApiVersion,
      documents: ["./src/types/shopify/admin/definitions/**/*.{js,ts}"],
      outputDir: "./src/types/shopify/admin/generated-types",
      declarations: false
    }),
  },
}

@paulomarg
Copy link
Contributor

Hey, thanks for raising this! Currently, graphql-codegen itself allows multiple projects, here is an example config:

  const config: IGraphQLConfig = {
    projects: {
      default: shopifyApiProject({
        apiType: ApiType.Admin,
        apiVersion: LATEST_API_VERSION,
        documents: ["./app/**/*.{js,ts,jsx,tsx}"],
        outputDir: "./app/types",
      }),
      storefront: shopifyApiProject({
        apiType: ApiType.Storefront,
        apiVersion: LATEST_API_VERSION,
        documents: ["./app/**/sf_*.{js,ts,jsx,tsx}"],
        outputDir: "./app/types",
      }),
    },
  };

There are a few caveats to consider when using

  • You won't be able to use both projects in a single file, because codegen currently only applies one schema per file. We're looking into adding a syntax for that (e.g. #graphql#storefront) to explicitly use a different project, but that's still being reviewed by the codegen team
    • For now, you'll need to make sure your documents are mutually exclusive in your config, so you can't have queries for both APIs in the same file.
  • You'll need to run separate commands for each of the projects to generate the types, e.g.:
    # Admin API
    npm run graphql-codegen
    # SF API
    npm run graphql-codegen -- -p storefront

Hope this helps!

@Subraiz
Copy link

Subraiz commented Jan 20, 2025

Taking the neat idea of of #graphql#storefront and #graphql#admin from @paulomarg I implemented a hacky way to accomplish this.

graphqlrc.config.ts

import { type ExpressionStatement, type TemplateLiteral } from "@babel/types";
import type { CodegenConfig } from "@graphql-codegen/cli";
import { ApiType, shopifyApiProject } from "@shopify/api-codegen-preset";
import type { IGraphQLConfig } from "graphql-config";

type Node = TemplateLiteral | ExpressionStatement;
type PluckConfig = CodegenConfig["pluckConfig"];

interface IProject extends ReturnType<typeof shopifyApiProject> {
  extensions: {
    codegen: {
      pluckConfig: {
        isGqlTemplateLiteral: (node: Node, options: PluckConfig) => boolean;
        pluckStringFromFile: (
          code: string,
          node: Node,
          options: PluckConfig,
        ) => string;
      };
      generates: Record<
        string,
        { config?: { scalars?: unknown; minify?: boolean } }
      >;
    };
  };
}

const OUTPUT_DIR = "./types";

const DOCUMENTS = [
  // "../../apps/web/src/**/*.{js,ts,jsx,tsx}",
  // "../api/src/**/*.{js,ts,jsx,tsx}",
  "../api/src/routers/store/testing/create-test-outreach.ts",
];
const API_VERSION = "2025-01";

const SCALARS = {
  ARN: "string",
  BigInt: "string",
  Color: "string",
  Money: "string",
  Date: "string",
  FormattedString: "string",
  DateTime: "string",
  Decimal: "string",
  HTML: "string",
  ISO8601DateTime: "string",
  URL: "string",
  UnsignedInt64: "string",
  JSON: "{ [key: string]: any }",
  UtcOffset: "string",
};

const createIsGqlTemplateLiteral = (apiType: ApiType = ApiType.Admin) => {
  return (
    node: TemplateLiteral | ExpressionStatement,
    options: CodegenConfig["pluckConfig"],
  ) => {
    // Check for internal gql comment: const QUERY = `#graphql ...`
    const regexPattern = `\\s*#graphql#${apiType}\\s*\\n`;
    const regex = new RegExp(regexPattern, "i");
    const hasInternalGqlComment =
      node.type === "TemplateLiteral" &&
      regex.test(node.quasis[0]?.value?.raw ?? "");

    if (hasInternalGqlComment) return true;

    // Check for leading gql comment: const QUERY = /* GraphQL */ `...`
    const { leadingComments } = node;
    const leadingComment = leadingComments?.[leadingComments?.length - 1];
    const leadingCommentValue = leadingComment?.value?.trim().toLowerCase();

    return leadingCommentValue === options?.gqlMagicComment;
  };
};

const pluckStringFromFile = (
  code: string,
  { start, end, leadingComments }: Node,
) => {
  let gqlTemplate = code
    // Slice quotes
    .slice((start ?? 0) + 1, (end ?? 0) - 1)
    // Annotate embedded expressions
    // e.g. ${foo} -> #REQUIRED_VAR=foo
    .replace(/\$\{([^}]*)\}/g, (_, m1) => "#REQUIRED_VAR=" + m1)
    .split("\\`")
    .join("`");

  const chunkStart = leadingComments?.[0]?.start ?? start ?? 0;
  const codeBeforeNode = code.slice(0, chunkStart);
  const [, varName] = codeBeforeNode.match(/\s(\w+)\s*=\s*$/) ?? [];

  if (varName) {
    // Annotate with the name of the variable that stores this gql template.
    // This is used to reconstruct the embedded expressions later.
    gqlTemplate += "#VAR_NAME=" + varName;
  }

  return gqlTemplate;
};

const AdminGraphQLProject: IProject = shopifyApiProject({
  apiType: ApiType.Admin,
  apiVersion: API_VERSION,
  documents: DOCUMENTS,
  outputDir: OUTPUT_DIR,
}) as unknown as IProject;

AdminGraphQLProject.extensions.codegen.generates[
  `${OUTPUT_DIR}/admin.types.d.ts`
]!.config = {
  minify: true,
  scalars: SCALARS,
};

AdminGraphQLProject.extensions.codegen.pluckConfig = {
  isGqlTemplateLiteral: createIsGqlTemplateLiteral(ApiType.Admin),
  pluckStringFromFile: pluckStringFromFile,
};

const StorefrontGraphQLProject: IProject = shopifyApiProject({
  apiType: ApiType.Storefront,
  apiVersion: API_VERSION,
  documents: DOCUMENTS,
  outputDir: OUTPUT_DIR,
}) as unknown as IProject;

StorefrontGraphQLProject.extensions.codegen.generates[
  `${OUTPUT_DIR}/storefront.types.d.ts`
]!.config = {
  minify: true,
  scalars: SCALARS,
};

StorefrontGraphQLProject.extensions.codegen.pluckConfig = {
  isGqlTemplateLiteral: createIsGqlTemplateLiteral(ApiType.Storefront),
  pluckStringFromFile: pluckStringFromFile,
};

const config: IGraphQLConfig = {
  schema: "https://shopify.dev/admin-graphql-direct-proxy/2025-01",
  documents: DOCUMENTS,
  projects: {
    default: AdminGraphQLProject,
    storefront: StorefrontGraphQLProject,
  },
};

export default config;

This allows me to follow the syntax of naming my queries accordingly e.g.

index.ts

import "../types/admin.generated.d.ts";
import "../types/storefront.generated.d.ts";

const adminProducts = await admin.request(`#graphql#admin
      query GetAdminProducts {
        products(first: 10) {
          edges {
            node {
              id
              title
            }
          }
        }
      }
    `);

    const storefrontProducts = await storefront.request(`#graphql#storefront
      query GetStorefrontProducts {
        products(first: 10) {
          edges {
            node {
              id
              title
            }
          }
        }
      }
    `);

And here is my package.json script so both types can be generated on the fly

{
  "scripts": {
    "generate-admin-types": "graphql-codegen -c graphqlrc.config.ts -w",
    "generate-storefront-types": "graphql-codegen -c graphqlrc.config.ts --project storefront -w",
    "generate-types": "pnpm run generate-admin-types & pnpm run generate-storefront-types"
  }
}

@lizkenyon
Copy link
Contributor

Hi @Subraiz Thanks for the update! 👌

If you would like, you could create a PR to add this as an example set up in the readme.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants