The prompt
Build a small internal web app on top of the GraphQL API "shop_api" (Microsoft Fabric API for GraphQL).
Data model (types, fields and relationships, exactly as the API names them):
- customers (query `customers`, by key, read-only): fields customer_id, name, country; relationships: addresses → addresses (OneToMany), orders → orders (OneToMany), audit_log → audit_log (OneToMany)
- categories (query `categories`, by key, read-only): fields id, name; relationships: products → products (OneToMany)
- products (query `products`, by key, read-only): fields product_id, name, category_id; relationships: category → categories (ManyToOne), order_items → order_items (OneToMany)
- addresses (query `addresses`, by key, read-only): fields address_id, customer_id, city; relationships: customer → customers (ManyToOne)
- orders (query `orders`, by key, read-only): fields order_id, customer_id, order_date, ship_to_id; relationships: customer → customers (ManyToOne), order_items → order_items (OneToMany)
- order_items (query `order_items`, by key, read-only): fields order_id, line_no, product_id, quantity; relationships: order → orders (ManyToOne), product → products (ManyToOne)
- audit_log (query `audit_logs`, read-only): fields event, customer_id, happened_at; relationships: customer → customers (ManyToOne)
Most important views:
- a list/detail page for order_items with its related records expanded
- a search box that filters the list pages; pagination with `first` and `after` on every list query
Access: the API needs a Microsoft Entra bearer token for the scope https://api.fabric.microsoft.com/.default; keep the token exchange server-side and never put a client secret in the browser. Endpoint and example queries below.
Endpoint: https://api.fabric.microsoft.com/v1/workspaces/{workspace-id}/graphqlapis/{api-id}/graphql
Example queries that show the relationships:
```graphql
query { customers(first: 10) { items { customer_id name country addresses { items { address_id customer_id city } } } } }
```
```graphql
query { categories(first: 10) { items { id name products { items { product_id name category_id } } } } }
```
```graphql
query { products(first: 10) { items { product_id name category_id category { id name } } } }
```
```graphql
query { addresses(first: 10) { items { address_id customer_id city customer { customer_id name country } } } }
```
```graphql
query { orders(first: 10) { items { order_id customer_id order_date ship_to_id customer { customer_id name country } } } }
```
Read-only: the API exposes no create/update/delete operations unless the type above says writable.
Backend: create the Supabase edge function `fabric-graphql` with exactly this source and call it from the pages through `src/lib/fabric.ts` below. The secrets ENTRA_TENANT_ID, ENTRA_CLIENT_ID, ENTRA_CLIENT_SECRET, FABRIC_GRAPHQL_ENDPOINT are Supabase secrets; ask me to add them, do not put values in code.
```ts
// supabase/functions/fabric-graphql/index.ts
// Forwards GraphQL queries from the app to Microsoft Fabric. The browser never sees an Entra secret:
// ENTRA_TENANT_ID, ENTRA_CLIENT_ID, ENTRA_CLIENT_SECRET and FABRIC_GRAPHQL_ENDPOINT are Supabase secrets.
// Service principal mode (default): one identity for the whole app (client credentials, scope .default).
// Per-user mode: the browser sends the signed-in user's own Fabric token in the x-fabric-token header
// (Supabase Auth with the Azure provider and the scope https://analysis.windows.net/powerbi/api/GraphQLApi.Execute.All); it is forwarded as is.
const TENANT = Deno.env.get("ENTRA_TENANT_ID") ?? "";
const CLIENT_ID = Deno.env.get("ENTRA_CLIENT_ID") ?? "";
const CLIENT_SECRET = Deno.env.get("ENTRA_CLIENT_SECRET") ?? "";
const ENDPOINT = Deno.env.get("FABRIC_GRAPHQL_ENDPOINT") ?? "https://api.fabric.microsoft.com/v1/workspaces/{workspace-id}/graphqlapis/{api-id}/graphql";
const SCOPE = "https://api.fabric.microsoft.com/.default";
const ALLOWED_ORIGIN = Deno.env.get("APP_ORIGIN") ?? "*";
const CORS = {
"Access-Control-Allow-Origin": ALLOWED_ORIGIN,
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type, x-fabric-token",
};
let cached: { token: string; expires: number } | null = null;
async function appToken(): Promise<string> {
if (cached && cached.expires > Date.now() + 60_000) return cached.token;
if (!TENANT || !CLIENT_ID || !CLIENT_SECRET) throw new Error("ENTRA_TENANT_ID, ENTRA_CLIENT_ID and ENTRA_CLIENT_SECRET must be set as secrets");
const r = await fetch(`https://login.microsoftonline.com/${TENANT}/oauth2/v2.0/token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ grant_type: "client_credentials", client_id: CLIENT_ID, client_secret: CLIENT_SECRET, scope: SCOPE }),
});
if (!r.ok) throw new Error(`token request failed: HTTP ${r.status}`);
const j = await r.json();
cached = { token: j.access_token, expires: Date.now() + Number(j.expires_in) * 1000 };
return cached.token;
}
Deno.serve(async (req) => {
if (req.method === "OPTIONS") return new Response("ok", { headers: CORS });
if (req.method !== "POST") return new Response("POST only", { status: 405, headers: CORS });
let body: { query?: unknown; variables?: unknown };
try { body = await req.json(); } catch { return new Response(JSON.stringify({ error: "JSON body expected" }), { status: 400, headers: { ...CORS, "Content-Type": "application/json" } }); }
const query = body.query;
if (typeof query !== "string" || /^\s*(mutation|subscription)\b/i.test(query)) {
return new Response(JSON.stringify({ error: "queries only" }), { status: 400, headers: { ...CORS, "Content-Type": "application/json" } });
}
const userToken = req.headers.get("x-fabric-token");
const bearer = userToken && userToken.length > 20 ? userToken : await appToken();
const r = await fetch(ENDPOINT, {
method: "POST",
headers: { Authorization: `Bearer ${bearer}`, "Content-Type": "application/json" },
body: JSON.stringify({ query, variables: body.variables ?? {} }),
});
return new Response(await r.text(), { status: r.status, headers: { ...CORS, "Content-Type": "application/json" } });
});
```
```ts
// src/lib/fabric.ts — the browser talks to the edge function only
import { supabase } from "@/integrations/supabase/client";
export async function fabricQuery<T>(query: string, variables: Record<string, unknown> = {}): Promise<T> {
const { data, error } = await supabase.functions.invoke("fabric-graphql", { body: { query, variables } });
if (error) throw error;
if (data?.errors?.length) throw new Error(data.errors[0].message);
return data.data as T;
}
```