[Overview](#overview)
[Installation](#installation)
[Initializing the client](#initializing-the-client)
[REST API](#rest-api)
-- [Documents](#documents)
-- [Key-Value store](#key-value-store)
-- [Users](#users)
-- [Assets](#assets)
[GraphQL API](#graphql-api)
[PIM Data Aggregator (PDA)](#pim-data-aggregator-pda)
[Practical examples](#practical-examples)
-- [1. Product listing with DataTable](#1-product-listing-with-datatable)
-- [2. Key-Value form with mutations](#2-key-value-form-with-mutations)

---

!!!info
This is a guide to help you understand how your Quable App can interact with the Quable PIM using the `@quable/quable-pim-js` SDK (v3.0.0).
!!!

---

## Overview

The [`@quable/quable-pim-js`](https://www.npmjs.com/package/@quable/quable-pim-js) library provides a high-level abstraction over the Quable PIM API. It supports:

- **REST API** — CRUD operations on documents, variants, links, assets, classifications, users, and key-values.
- **GraphQL API** — Flexible queries with variables.
- **PIM Data Aggregator (PDA)** — Advanced data extraction and aggregation for bulk operations.

---

## Installation

The SDK is already included in the app template. If you need to install it manually:

```bash
npm install @quable/quable-pim-js
# or
yarn add @quable/quable-pim-js
```

**Requirements:**
- Node.js >= 18.0.0
- A valid API token from your Quable PIM instance (Administration > API Token)

---

## Initializing the client

Create a `QuablePimClient` instance with your instance name and API token:

```ts
import { QuablePimClient } from "@quable/quable-pim-js";

const client = new QuablePimClient({
  instanceName: "my-instance",
  apiToken: "my-api-token",
});
```

In the template, the client is typically created inside Server Actions using credentials from the database:

```ts
"use server";

import { QuablePimClient } from "@quable/quable-pim-js";
import prisma from "../prisma";
import { getCurrentSession } from "../session";

async function getQuablePimClient() {
  const session = await getCurrentSession();
  if (!session) throw new Error("Session not found");

  const quableInstance = await prisma.quableInstance.findUniqueOrThrow({
    where: { id: session.quableInstanceId },
  });

  return new QuablePimClient({
    apiToken: quableInstance.token,
    instanceName: quableInstance.name,
  });
}
```

---

## REST API

The REST API is accessible via `client.API.REST`. Each resource has its own set of methods.

### Documents

```ts
// Get all documents (with optional filters)
const documents = await client.API.REST.Document.getAll({});

// Get all documents with filters
const filtered = await client.API.REST.Document.getAll({
  limit: 10,
  id: "my-document-id",
  active: true,
});

// Create a document
const newDoc = await client.API.REST.Document.create({
  id: "new-document-id",
  documentTypeId: "product",
  classificationId: "category-1",
  attributes: {
    product_name: { en_US: "My Product", fr_FR: "Mon Produit" },
  },
});

// Update a document
const updatedDoc = await client.API.REST.Document.update("document-id", {
  attributes: {
    product_name: { en_US: "Updated Name" },
  },
});

// Delete a document
await client.API.REST.Document.delete("document-id");
```

Each document returned by `getAll()` includes:
- `id` — The document code
- `documentType` — `{ id: string }` the document type
- `attributes` — Localized attribute values
- `mainAssetThumbnailUrl` — Thumbnail URL of the main asset
- `legacyId` — Legacy numeric ID

---

### Key-Value store

The Key-Value store allows your app to persist configuration data in the PIM:

```ts
import { KeyValueModel } from "@quable/quable-pim-js";

// Create a key-value pair
const kv: KeyValueModel = await client.API.REST.KeyValue.create({
  id: "my-config-key",
  value: "my-config-value",
  isPublic: true,
  isProtected: false,
});
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | string | The unique key identifier |
| `value` | string | The stored value |
| `isPublic` | boolean | Whether the key is publicly accessible |
| `isProtected` | boolean | Whether the key is protected from deletion |

---

### Users

```ts
// Get users (useful for token validation during installation)
const users = await client.API.REST.User.getAll({
  limit: 1,
  type: "api",
});
```

This is used in the install route to validate the API token:

```ts
// src/app/install/route.ts
const pimClient = new QuablePimClient({
  apiToken: quableAuthToken,
  instanceName: quableInstanceName,
});

// This throws if the token is invalid
await pimClient.API.REST.User.getAll({ limit: 1, type: "api" });
```

---

### Assets

```ts
// Get an asset by code
const asset = await client.API.REST.Asset.get("asset-code");

// Update an asset
await client.API.REST.Asset.update("asset-code", {
  attributes: { alt_text: { en_US: "Product photo" } },
});
```

---

## GraphQL API

For more flexible queries, use the GraphQL API:

```ts
// Simple query
const result = await client.API.GraphQL.query(`
  {
    documents(first: 10) {
      edges {
        node {
          id
          code
          attributes
        }
      }
    }
  }
`);

// Query with variables
const result = await client.API.GraphQL.query(
  `query GetDocument($code: String!) {
    document(code: $code) {
      id
      code
      attributes
    }
  }`,
  { code: "my-document-code" }
);
```

---

## PIM Data Aggregator (PDA)

The PDA module is designed for extracting and aggregating data from documents with complex hierarchies.

### Single query

```ts
// Get aggregated data for specific document IDs
const data = await client.API.PDA.Query.getData({
  documentIds: ["doc-1", "doc-2", "doc-3"],
});
```

### Bulk operations

For large datasets, use the async operation pattern:

```ts
// 1. Create a bulk aggregation operation
const operation = await client.API.PDA.Operation.create({
  documentTypeId: "product",
});

// 2. Check operation status
const status = await client.API.PDA.Operation.get(operation.id);

// 3. Download results when complete
const results = await client.API.PDA.Operation.downloadAndUnzip(operation.id);
```

---

## Practical examples

### 1. Product listing with DataTable

This example shows how to fetch documents from the PIM and display them in a data table.

#### Server Action (src/lib/actions/products.ts)

```ts
"use server";

import { QuablePimClient } from "@quable/quable-pim-js";
import prisma from "../prisma";
import { getCurrentSession } from "../session";

export async function getProducts(): Promise<
  { image: string | null; name: string; code: string; productType: string }[]
> {
  try {
    const session = await getCurrentSession();
    if (!session) throw new Error("Session not found");

    const quableInstance = await prisma.quableInstance.findUniqueOrThrow({
      where: { id: session.quableInstanceId },
    });

    const quablePimClient = new QuablePimClient({
      apiToken: quableInstance.token,
      instanceName: quableInstance.name,
    });

    const products = await quablePimClient.API.REST.Document.getAll({});
    return products.map((product) => {
      const productName = product.attributes[`${product.documentType.id}_name`];
      return {
        image: product.mainAssetThumbnailUrl,
        name: productName[session.dataLocale]
          ? productName[session.dataLocale]
          : product.id,
        code: product.id,
        productType: product.documentType.id,
        directUrl: `https://${quableInstance.name}.quable.com/#classification/product-new/${product.legacyId}/edit`,
      };
    });
  } catch (error) {
    console.error(error);
    return [];
  }
}
```

#### Client Component (src/app/[session]/product/page.tsx)

```tsx
"use client";

import { useTranslations } from "next-intl";
import { DataTable, EmptyState } from "@quable/ui";
import { useQuery } from "@tanstack/react-query";
import { GridColDef } from "@mui/x-data-grid-pro";
import { Typography } from "@mui/material";
import { OpenInNew } from "@mui/icons-material";
import { getProducts } from "@/lib/actions/products";
import Image from "next/image";

export default function ProductsPage() {
  const t = useTranslations("products_page");
  const { data: products, isLoading } = useQuery({
    queryKey: ["products"],
    queryFn: () => getProducts(),
  });

  const columns: GridColDef[] = [
    {
      field: "image",
      headerName: "",
      sortable: false,
      renderCell: (params) => {
        if (!params.value) return <></>;
        return <Image src={params.value} alt={params.row.name} width={100} height={100} />;
      },
    },
    {
      field: "code",
      headerName: t("columns.product_code"),
      sortable: false,
      flex: 1,
    },
    {
      field: "name",
      headerName: t("columns.product_name"),
      sortable: false,
      flex: 1,
    },
    {
      field: "productType",
      headerName: t("columns.product_type"),
      sortable: false,
      flex: 1,
    },
    {
      field: "action",
      headerName: "",
      sortable: false,
      align: "right",
      renderCell: (params) => (
        <OpenInNew
          sx={{ cursor: "pointer", color: "text.secondary" }}
          fontSize="small"
          onClick={() => window.open(params.row.directUrl, "_blank", "noopener,noreferrer")}
        />
      ),
    },
  ];

  return (
    <div className="p-4">
      <Typography sx={{ mb: 2 }} fontWeight={600} variant="body-lg">
        {t("title")}
      </Typography>
      <DataTable
        loading={isLoading}
        columns={columns}
        rowCount={products?.length || 0}
        rows={products || []}
        getRowId={(row) => row.code}
        rootProps={{ style: { height: "90vh" } }}
        slots={{
          noRowsOverlay: () => (
            <EmptyState title={t("no_products")} description={t("no_products_description")} />
          ),
        }}
      />
    </div>
  );
}
```

**Key patterns:**
- **`useQuery`** fetches data when the component mounts and caches the result.
- **`DataTable`** from `@quable/ui` wraps MUI DataGrid Pro with Quable styling.
- **`EmptyState`** from `@quable/ui` provides a consistent empty state UI.
- **`GridColDef`** defines the columns with custom `renderCell` for images and actions.

---

### 2. Key-Value form with mutations

This example shows how to write data to the PIM using a form.

#### Server Action (src/lib/actions/key-value.ts)

```ts
"use server";

import { getCurrentSession } from "../session";
import prisma from "../prisma";
import { KeyValueModel, QuablePimClient } from "@quable/quable-pim-js";
import { getTranslations } from "next-intl/server";

export async function addKeyValue(
  key: string,
  value: string
): Promise<{ keyValue?: KeyValueModel | null; error?: string | null }> {
  const t = await getTranslations("common");
  try {
    const session = await getCurrentSession();
    if (!session) throw new Error("Session not found");

    const quableInstance = await prisma.quableInstance.findUniqueOrThrow({
      where: { id: session.quableInstanceId },
    });

    const quablePimClient = new QuablePimClient({
      apiToken: quableInstance.token,
      instanceName: quableInstance.name,
    });

    const keyValue = await quablePimClient.API.REST.KeyValue.create({
      isProtected: false,
      isPublic: true,
      id: key,
      value: value,
    });

    return { keyValue, error: null };
  } catch (error) {
    if (typeof error === "object") {
      const err = error as { response?: { code?: number; message?: string } };
      if (err.response?.code === 400) {
        return { keyValue: null, error: err.response.message };
      }
    }
    return { keyValue: null, error: t("toasts.add_key_value_error") };
  }
}
```

#### Client Component

```tsx
"use client";

import { addKeyValue } from "@/lib/actions/key-value";
import { TextField, Button } from "@quable/ui";
import { useMutation } from "@tanstack/react-query";
import { useForm } from "react-hook-form";
import { toast } from "react-toastify";

export default function KeyValueForm() {
  const { register, formState, handleSubmit, reset } = useForm<{
    key: string;
    value: string;
  }>({ defaultValues: { key: "", value: "" }, mode: "onChange" });

  const { isPending, mutateAsync } = useMutation({
    mutationFn: (data: { key: string; value: string }) =>
      addKeyValue(data.key, data.value),
    onSuccess: (data) => {
      if (data.keyValue) {
        toast.success("Key-value added successfully");
        reset();
      } else {
        toast.error(data.error);
      }
    },
  });

  return (
    <form noValidate onSubmit={handleSubmit((data) => mutateAsync(data))}>
      <TextField
        disabled={isPending}
        label="Key"
        fullWidth
        error={!!formState.errors.key}
        {...register("key", { required: "Key is required" })}
      />
      <TextField
        disabled={isPending}
        label="Value"
        fullWidth
        error={!!formState.errors.value}
        {...register("value", { required: "Value is required" })}
      />
      <Button isLoading={isPending} variant="contained" type="submit">
        Save
      </Button>
    </form>
  );
}
```

**Key patterns:**
- **`useMutation`** wraps the Server Action for write operations.
- **`react-hook-form`** handles validation and form state.
- **Error handling** in the Server Action returns structured errors that the client can display.
- **`toast`** from react-toastify provides user feedback.

!!!success
You now know how to interact with the Quable PIM API using the `@quable/quable-pim-js` SDK. Next, we'll explore how to subscribe to and handle webhook events.
!!!
