Shopifyアプリ – Remix版scaffoldのgraphqlの理解

Shopify

はじめに

  • codegenでgraphqlの開発がどのように自動化できるのか調査する
  • graphqlのきれいなコードの収め方を考える

参考

ソース確認

package.jsonの確認

  • @shopify/api-codegen-presetがインストール済み
  • スクリプトに、 graphql-codegenが設定済み
  • 追加で、npm add @shopify/admin-api-client @shopify/storefront-api-client を実行
{
  "name": "xxxxx",
  "private": true,
  "scripts": {
    "build": "remix vite:build",
    "dev": "shopify app dev",
    "config:link": "shopify app config link",
    "generate": "shopify app generate",
    "deploy": "shopify app deploy",
    "config:use": "shopify app config use",
    "env": "shopify app env",
    "start": "remix-serve ./build/server/index.js",
    "docker-start": "npm run setup && npm run start",
    "setup": "prisma generate && prisma migrate deploy",
    "lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .",
    "shopify": "shopify",
    "prisma": "prisma",
    "graphql-codegen": "graphql-codegen",
    "vite": "vite"
  },
  "type": "module",
  "engines": {
    "node": "^18.20 || ^20.10 || >=21.0.0"
  },
  "dependencies": {
    "@prisma/client": "^5.11.0",
    "@remix-run/dev": "^2.7.1",
    "@remix-run/node": "^2.7.1",
    "@remix-run/react": "^2.7.1",
    "@remix-run/serve": "^2.7.1",
    "@shopify/admin-api-client": "^1.0.1",
    "@shopify/app-bridge-react": "^4.1.2",
    "@shopify/polaris": "^12.0.0",
    "@shopify/shopify-app-remix": "^3.0.2",
    "@shopify/shopify-app-session-storage-prisma": "^5.0.2",
    "@shopify/storefront-api-client": "^1.0.1",
    "isbot": "^5.1.0",
    "prisma": "^5.11.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "vite-tsconfig-paths": "^5.0.1"
  },
  "devDependencies": {
    "@remix-run/eslint-config": "^2.7.1",
    "@shopify/api-codegen-preset": "^1.0.1",
    "@types/eslint": "^8.40.0",
    "@types/node": "^22.2.0",
    "@types/react": "^18.2.31",
    "@types/react-dom": "^18.2.14",
    "eslint": "^8.42.0",
    "eslint-config-prettier": "^9.1.0",
    "prettier": "^3.2.4",
    "typescript": "^5.2.2",
    "vite": "^5.1.3"
  },
  "workspaces": [
    "extensions/*"
  ],
  "trustedDependencies": [
    "@shopify/plugin-cloudflare"
  ],
  "resolutions": {
    "undici": "6.13.0"
  },
  "overrides": {
    "undici": "6.13.0"
  },
  "author": "xxxxx"
}

.graphqlrc.tsの確認

import fs from "fs";
import { LATEST_API_VERSION } from "@shopify/shopify-api";
import { shopifyApiProject, ApiType } from "@shopify/api-codegen-preset";
import type { IGraphQLConfig } from "graphql-config";

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

  let extensions: string[] = [];
  try {
    extensions = fs.readdirSync("./extensions");
  } catch {
    // ignore if no extensions
  }

  for (const entry of extensions) {
    const extensionPath = `./extensions/${entry}`;
    const schema = `${extensionPath}/schema.graphql`;
    if (!fs.existsSync(schema)) {
      continue;
    }
    config.projects[entry] = {
      schema,
      documents: [`${extensionPath}/**/*.graphql`],
    };
  }

  return config;
}

module.exports = getConfig();

graphql-codegenの実行

% npm run graphql-codegen
> graphql-codegen
> graphql-codegen

✔ Parse Configuration
✔ Generate out

出力されたファイル

  • types/admin-2024-07.schema.json
  • types/admin.generated.d.ts
  • types/admin.types.d.ts

出力ファイルの理解

  • projects.defaultの設定
    • スキーマは、./app/types/*.schema.json
    • ./app/**/以下にAdmin API用のドキュメントを配置。codegenでは./app/types/*.tsを出力
  • 他のプロジェクトを追加する場合
    • extensions/プロジェクト名/のディレクトリを作成
    • スキーマ:extensions/プロジェクト名/shema.graphql
    • ドキュメント:extensions/プロジェクト名/**/*.graphql

クエリの作成例

  • app/graphql/admin/OrdersQuery.ts
export const ADMIN_ORDERS_QUERY = `#graphql
    {
        orders(first: 50, reverse: true) {
            edges {
                node {
                    id
                    createdAt
                    customer {
                        firstName
                    }
                    displayFulfillmentStatus
                    email
                    originalTotalPriceSet {
                        presentmentMoney {
                            amount
                            currencyCode
                        }
                    }
                }
            }
        }
    }
` as const;
  • 呼び出し部コード
import {
    IndexTable,
    Page, 
} from "@shopify/polaris";
import { authenticate } from "../shopify.server";
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import type { LoaderFunctionArgs } from "@remix-run/node";
import { OrderEdge, Order } from '../types/admin.types.d';
import {ADMIN_ORDERS_QUERY} from '../graphql/admin/OrdersQuery';

export const meta = () => {
    return [{ title: "Orders" }];
};

export const loader = async ({ request }: LoaderFunctionArgs) => {
    const { admin } = await authenticate.admin(request);
    const response = await admin.graphql(ADMIN_ORDERS_QUERY);
    const responseJson = await response.json();
    const orders: Order[] = responseJson?.data?.orders?.edges?.map((row: OrderEdge) => row.node);
    
    return json({
        orders
    });
};

export default function Orders() {
    const { orders } = useLoaderData<{ orders: Order[] }>();
    const resourceName = {
        singular: 'order',
        plural: 'orders',
    };
    const rowMarkup = orders.map(
        (
            { id, originalTotalPriceSet, createdAt, email, customer },
            index,
        ) => (
            <IndexTable.Row
                id={id}
                key={id}
                position={index}
            >
                <IndexTable.Cell>#{id?.replace('gid://shopify/Order/', '')}</IndexTable.Cell>
                <IndexTable.Cell>{customer?.firstName}</IndexTable.Cell>
                <IndexTable.Cell>{email}</IndexTable.Cell>
                <IndexTable.Cell>{originalTotalPriceSet?.presentmentMoney?.amount}</IndexTable.Cell>
                <IndexTable.Cell>{createdAt}</IndexTable.Cell>
            </IndexTable.Row>
        ),
    );
    return (
        <Page title="Orders" fullWidth>
            <IndexTable
                resourceName={resourceName}
                itemCount={orders.length}
                headings={[
                    { title: 'Order Id' },
                    { title: 'Name' },
                    { title: 'Email' },
                    { title: 'Total' },
                    { title: 'Created' },
                ]}
                selectable={false}
            >
                {rowMarkup}
            </IndexTable>
        </Page>
    );
}

関連記事

カテゴリー

アーカイブ

Lang »