跳至主要内容

如何将 Prisma Postgres 与 Shopify 结合使用

25 分钟

介绍

Shopify 是一个流行的电子商务商店建设平台。本指南将向您展示如何将 Shopify 应用连接到 Prisma Postgres 数据库,以便为产品创建内部笔记。

先决条件

1. 设置项目

注意

如果您没有安装 Shopify CLI,可以使用 npm install -g @shopify/cli 进行安装。

首先,使用 Shopify CLI 初始化一个新的 Shopify 应用。

shopify app init

在设置过程中,系统会提示您自定义应用。别担心,只需遵循以下推荐选项即可快速上手,并确保您的应用设置成功。

信息
  • 开始构建您的应用: 构建 Remix 应用(推荐)
  • 对于您的 Remix 模板,您想要哪种语言: JavaScript
  • 应用名称: prisma-store (名称不能包含 shopify

导航到 prisma-store 目录。

cd prisma-store

2. 设置 Prisma

Prisma 已预安装在您的项目中,但让我们花点时间将其更新到最新版本。这可确保您在构建应用时能够访问最新功能、改进和最佳体验。

您将切换到 Prisma Postgres 数据库,因此请删除 prisma 目录中的 migrations 文件夹和 dev.sqlite 文件。

您需要更新 schema.prisma 文件中的一些内容,以使其与 Remix 和 Prisma Postgres 配合使用。

  • 切换到新的 prisma-client 生成器。
  • 将提供程序更新为 postgresql
  • 将 URL 更新为新的数据库 URL。
prisma/schema.prisma
generator client {
provider = "prisma-client-js"
provider = "prisma-client"
output = "../app/generated/prisma"
}

datasource db {
provider = "sqlite"
provider = "postgresql"
url = "file:../dev.db"
}

model Session {
// ... existing model
}

创建一个 prisma.config.ts 文件来配置 Prisma

prisma.config.ts
import 'dotenv/config'
import { defineConfig, env } from 'prisma/config';

export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
path: 'prisma/migrations',
},
datasource: {
url: env('DATABASE_URL'),
},
});
注意

由于 Shopify 应用通常预装了 dotenv,因此您应该已经可以使用它。如果还没有,请使用以下命令安装:

npm install dotenv

为了让您的应用能够为每个产品存储笔记,让我们向 Prisma schema 添加一个新的 ProductNote 模型。

此模型将允许您通过 productGid 字段在数据库中保存和组织与单个产品关联的笔记。

prisma/schema.prisma
generator client {
provider = "prisma-client"
output = "../app/generated/prisma"
}

datasource db {
provider = "postgresql"
}

model Session {
// ... existing model
}

model ProductNote {
id String @id @default(uuid())
productGid String
body String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

接下来,Prisma 将需要更新到最新版本。运行

npm install prisma @types/pg --save-dev
npm install @prisma/client @prisma/adapter-pg pg
信息

如果你使用的是其他数据库提供程序(MySQL、SQL Server、SQLite),请安装相应的驱动程序适配器包,而不是 @prisma/adapter-pg。有关更多信息,请参阅 数据库驱动程序

Prisma Postgres 允许您即时创建新数据库,您可以在初始化项目时通过添加 --db 标志来同时创建新数据库

npx prisma init --db

完成提示后,即可访问新数据库。

  1. 打开:

    • 登录并选择您新创建的数据库项目。
  2. 获取您的数据库连接字符串。

    • 单击 连接 按钮。
    • 复制显示的连接字符串。它应该类似于这样
    DATABASE_URL="postgresql://user:password@host:5432/database?sslmode=require"
  3. 配置您的环境

    • 在项目的根目录中创建一个新的 .env 文件。
    • 将您刚刚复制的 DATABASE_URL 粘贴到此文件中。
  4. 应用数据库架构

    • 运行以下命令创建您的表并准备好您的数据库
    npx prisma migrate dev --name init

    然后生成 Prisma Client

    npx prisma generate

现在,在继续之前,让我们更新您的 db.server.ts 文件,以使用新生成的带驱动程序适配器的 Prisma 客户端

app/db.server.ts
import { PrismaClient } from "@prisma/client";
import { PrismaClient } from "./generated/prisma/client.js";
import { PrismaPg } from "@prisma/adapter-pg";

const adapter = new PrismaPg({
connectionString: process.env.DATABASE_URL,
});

if (process.env.NODE_ENV !== "production") {
if (!global.prismaGlobal) {
global.prismaGlobal = new PrismaClient();
global.prismaGlobal = new PrismaClient({ adapter });
}
}

const prisma = global.prismaGlobal ?? new PrismaClient();
const prisma = global.prismaGlobal ?? new PrismaClient({ adapter });

export default prisma;
警告

建议将 app/generated/prisma 添加到您的 .gitignore 文件中。

3. 创建您的 Remix 模型

为了保持项目井然有序,我们来创建一个新的 models/ 文件夹。在此文件夹中,添加一个名为 notes.server.js 的文件。这将是所有与笔记相关的逻辑的存放地,随着应用程序的增长,您的代码库将更容易管理。

notes.server.js 文件将包含两个函数

  • getNotes - 这将获取给定产品的所有笔记。
  • createNote - 这将为给定产品创建新笔记。

首先从 db.server.ts 导入 Prisma 客户端并创建 getNotes 函数。

models/notes.server.js
import prisma from "../db.server";

export const getNotes = async (productGid) => {
const notes = await prisma.productNote.findMany({
where: { productGid: productGid.toString() },
orderBy: { createdAt: "desc" },
});
return notes;
};

为了让用户能够向您的数据库添加新笔记,我们来在 notes.server.js 中创建一个使用 prisma.productNote.create 的函数。

models/notes.server.js
import prisma from "../db.server";

export const getNotes = async (productGid) => {
const notes = await prisma.productNote.findMany({
where: { productGid: productGid.toString() },
orderBy: { createdAt: "desc" },
});
return notes;
};

export const createNote = async (note) => {
const newNote = await prisma.productNote.create({
data: {
body: note.body,
productGid: note.productGid,
},
});
return newNote;
};

4. 创建您的布局路由

在这些函数能够被调用之前,我们的路由需要一个布局来承载它们。这个布局路由将包含一个用于选择产品的按钮,并将作为您的 ProductNotes 路由的父路由,从而保持您的应用程序井然有序且用户友好。

4.1. 创建 ProductNotesLayout 组件

首先创建文件夹 routes/app.product-notes.jsx 并在其中添加 ProductNotesLayout 组件

app/routes/app.product-notes.jsx
import { Page, Layout } from "@shopify/polaris";

export default function ProductNotesLayout() {
return (
<Page title="Product Notes">
<Layout>
<Layout.Section></Layout.Section>
</Layout>
</Page>
);
}

接下来,创建 selectProduct 函数和 Button,让用户选择一个产品。

app/routes/app.product-notes.jsx
import { useNavigate } from "@remix-run/react";
import { Page, Layout } from "@shopify/polaris";
import { Button, Page, Layout } from "@shopify/polaris";

export default function ProductNotesLayout() {
const navigate = useNavigate();

async function selectProduct() {
const products = await window.shopify.resourcePicker({
type: "product",
action: "select",
});
const selectedGid = products[0].id;
navigate(`/app/product-notes/${encodeURIComponent(selectedGid)}`);
}

return (
<Page title="Product Notes">
<Layout>
<Layout.Section>
<Button onClick={selectProduct} fullWidth size="large">
Select Product
</Button>
</Layout.Section>
</Layout>
</Page>
);
}

Remix 渲染提供了渲染嵌套路由的功能。在 routes/app.product-notes.jsx 文件中添加一个 <Outlet />ProductNotes 路由将在此处渲染。

app/routes/app.product-notes.jsx
import { useNavigate } from "@remix-run/react";
import { Outlet, useNavigate } from "@remix-run/react";
import { Page, Button, Layout } from "@shopify/polaris";

export default function ProductNotesLayout() {
const navigate = useNavigate();

async function selectProduct() {
const products = await window.shopify.resourcePicker({
type: "product",
action: "select",
});
const selectedGid = products[0].id;
navigate(`/app/product-notes/${encodeURIComponent(selectedGid)}`);
}

return (
<Page title="Product Notes">
<Layout>
<Layout.Section>
<Button onClick={selectProduct} fullWidth size="large">
Select Product
</Button>
</Layout.Section>
<Outlet />
</Layout>
</Page>
);
}

4.2. 将 ProductNotesLayout 添加到侧边栏

如果您运行 npm run dev,您将无法看到 Product Notes 路由。要解决此问题,您需要将 ProductNotesLayout 添加到 app.jsx 文件中,以便它显示在侧边栏中。

app/routes/app.jsx
import { Link, Outlet, useLoaderData, useRouteError } from "@remix-run/react";
import { boundary } from "@shopify/shopify-app-remix/server";
import { AppProvider } from "@shopify/shopify-app-remix/react";
import { NavMenu } from "@shopify/app-bridge-react";
import polarisStyles from "@shopify/polaris/build/esm/styles.css?url";
import { authenticate } from "../shopify.server";

export const links = () => [{ rel: "stylesheet", href: polarisStyles }];

export const loader = async ({ request }) => {
await authenticate.admin(request);

return { apiKey: process.env.SHOPIFY_API_KEY || "" };
};

export default function App() {
const { apiKey } = useLoaderData();

return (
<AppProvider isEmbeddedApp apiKey={apiKey}>
<NavMenu>
<Link to="/app" rel="home">
Home
</Link>
<Link to="/app/product-notes">Product Notes</Link>
</NavMenu>
<Outlet />
</AppProvider>
);
}

// Shopify needs Remix to catch some thrown responses, so that their headers are included in the response.
export function ErrorBoundary() {
return boundary.error(useRouteError());
}

export const headers = (headersArgs) => {
return boundary.headers(headersArgs);
};

5. 创建您的产品笔记路由

目前,如果您运行 npm run dev 并导航到 Product Notes 路由,则在选择产品后将看不到任何内容。

按照以下步骤创建产品笔记路由

创建一个新的 routes/app/app.notes.$productGid.jsx 文件,该文件将把 productGid 作为参数,并返回与该产品关联的产品笔记以及一个用于创建新笔记的表单。

app/routes/app/app.notes.$productGid.jsx
export default function ProductNotes() {
return (
<></>
);
}

5.1. 渲染笔记

加载时,路由需要获取产品的笔记并显示它们。

向路由添加一个 loader 函数

app/routes/app/app.notes.$productGid.jsx
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { getNotes } from "../models/note.server";

export const loader = async ({ params }) => {
const { productGid } = params;
const notes = await getNotes(productGid);
return json({ notes, productGid });
};

export default function ProductNotes() {
const { notes, productGid } = useLoaderData();

return (
<></>
);
}

ProductNotes 组件中,使用 Polaris 组件列出笔记。

app/routes/app/app.notes.$productGid.jsx
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { getNotes } from "../models/note.server";
import { Card, Layout, Text, BlockStack } from "@shopify/polaris";

export const loader = async ({ params }) => {
const { productGid } = params;
const notes = await getNotes(productGid);
return json({ notes, productGid });
};

export default function ProductNotes() {
const { notes, productGid } = useLoaderData();
return (
<>
<Layout.Section>
<BlockStack gap="200">
{notes.length === 0 ? (
<Text as="p" variant="bodyMd" color="subdued">
No notes yet.
</Text>
) : (
notes.map((note) => (
<Card key={note.id} sectioned>
<BlockStack gap="100">
{note.body && (
<Text as="p" variant="bodyMd">
{note.body}
</Text>
)}
<Text as="p" variant="bodySm" color="subdued">
Added: {new Date(note.createdAt).toLocaleString()}
</Text>
</BlockStack>
</Card>
))
)}
</BlockStack>
</Layout.Section>
</>
);
}

您应该会看到“暂无笔记”。如果是这样,您就走对了。

5.2. 添加表单

需要向路由中添加一些内容才能创建新笔记。

  • 向路由添加一个 action 函数。
  • 创建笔记时显示 Toast 通知。
  • models/note.server.js 导入 createNote 函数。
  • 导入 useActionDatauseAppBridge
app/routes/app/app.notes.$productGid.jsx
import { json, redirect } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { useLoaderData, useActionData } from "@remix-run/react";
import { getNotes } from "../models/note.server";
import { getNotes, createNote } from "../models/note.server";
import { Card, Layout, Text, BlockStack } from "@shopify/polaris";
import { useAppBridge } from "@shopify/app-bridge-react";

export const loader = async ({ params }) => {
const { productGid } = params;
const notes = await getNotes(productGid);
return json({ notes, productGid });
};

export const action = async ({ request, params }) => {
const formData = await request.formData();
const body = formData.get("body")?.toString() || null;
const { productGid } = params;

await createNote({ productGid, body });
return redirect(`/app/product-notes/${encodeURIComponent(productGid)}`);
};

export default function ProductNotes() {
const { notes, productGid } = useLoaderData();
const actionData = useActionData();
const app = useAppBridge();

useEffect(() => {
if (actionData?.ok) {
app.toast.show("Note saved", { duration: 3000 });
setBody("");
}
}, [actionData, app]);

return (
<>
<Layout.Section>
<BlockStack gap="200">
{notes.length === 0 ? (
<Text as="p" variant="bodyMd" color="subdued">
No notes yet.
</Text>
) : (
notes.map((note) => (
<Card key={note.id} sectioned>
<BlockStack gap="100">
{note.body && (
<Text as="p" variant="bodyMd">
{note.body}
</Text>
)}
<Text as="p" variant="bodySm" color="subdued">
Added: {new Date(note.createdAt).toLocaleString()}
</Text>
</BlockStack>
</Card>
))
)}
</BlockStack>
</Layout.Section>
</>
);
}

现在,您可以构建将调用 action 函数的表单。

app/routes/app/app.notes.$productGid.jsx
import { json, redirect } from "@remix-run/node";
import { useLoaderData, useActionData } from "@remix-run/react";
import { getNotes, createNote } from "../models/note.server";
import { Card, Layout, Text, BlockStack } from "@shopify/polaris";
import { Card, Layout, Text, BlockStack, Form, FormLayout, TextField, Button } from "@shopify/polaris";
import { useAppBridge } from "@shopify/app-bridge-react";

export const loader = async ({ params }) => {
const { productGid } = params;
const notes = await getNotes(productGid);
return json({ notes, productGid });
};

export const action = async ({ request, params }) => {
const formData = await request.formData();
const body = formData.get("body")?.toString() || null;
const { productGid } = params;

await createNote({ productGid, body });
return redirect(`/app/product-notes/${encodeURIComponent(productGid)}`);
};

export default function ProductNotes() {
const { notes, productGid } = useLoaderData();
const actionData = useActionData();
const app = useAppBridge();

useEffect(() => {
if (actionData?.ok) {
app.toast.show("Note saved", { duration: 3000 });
setBody("");
}
}, [actionData, app]);

return (
<>
<Layout.Section>
<Card sectioned>
<Form method="post">
<FormLayout>
<BlockStack gap="200">
<input type="hidden" name="productGid" value={productGid} />
<TextField
label="Note"
value={body}
onChange={setBody}
name="body"
autoComplete="off"
multiline={4}
/>
<Button submit primary>
Add Note
</Button>
</BlockStack>
</FormLayout>
</Form>
</Card>
</Layout.Section>
<Layout.Section>
<BlockStack gap="200">
{notes.length === 0 ? (
<Text as="p" variant="bodyMd" color="subdued">
No notes yet.
</Text>
) : (
notes.map((note) => (
<Card key={note.id} sectioned>
<BlockStack gap="100">
{note.body && (
<Text as="p" variant="bodyMd">
{note.body}
</Text>
)}
<Text as="p" variant="bodySm" color="subdued">
Added: {new Date(note.createdAt).toLocaleString()}
</Text>
</BlockStack>
</Card>
))
)}
</BlockStack>
</Layout.Section>
</>
);
}

现在您应该能够为产品添加笔记并看到它显示出来。

6. 测试您的路由

运行 npm run dev 并导航到 Product Notes 路由。

  • 导航到侧边栏上的“产品笔记”
  • 选择产品
  • 添加笔记
  • 验证笔记是否正确显示和保存。

下一步

现在您有一个可用的 Shopify 应用连接到 Prisma Postgres 数据库,您可以

  • 使用更多模型和关系扩展您的 Prisma schema
  • 添加创建/更新/删除路由和表单
  • 使用 Prisma Postgres 启用查询缓存以获得更好的性能

更多信息


与 Prisma 保持联系

通过以下方式与我们保持联系,继续你的 Prisma 之旅: 我们的活跃社区。保持信息灵通,参与其中,并与其他开发人员协作。

我们真诚地感谢你的参与,并期待你成为我们社区的一部分!

© . This site is unofficial and not affiliated with Prisma Data, Inc.