一键导入
frontend-profile-batch-query
Use when fetching user profiles in bulk (workaround for PostgREST cross-schema join limitation)
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use when fetching user profiles in bulk (workaround for PostgREST cross-schema join limitation)
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| name | frontend-profile-batch-query |
| description | Use when fetching user profiles in bulk (workaround for PostgREST cross-schema join limitation) |
Pattern for fetching multiple user profiles when PostgREST cannot join across schema boundaries.
Technical Constraint:
profiles.id → auth.users(id) (FK to auth schema)organization_members.user_id → auth.users(id) (FK to auth schema)profiles and organization_membersFailed approach:
// ❌ IMPOSSIBLE - PostgREST cannot traverse auth.users
supabase.from("organization_members").select(`
id,
user_id,
profiles ( // ❌ No direct FK relationship
email
)
`);
Error: Could not find a relationship between 'organization_members' and 'profiles' in the schema cache
Working approach:
// ✅ Batch query using .in() operator
supabase.from("profiles").select("id, email, whitelisted").in("id", userIds);
File: hooks/[Scope]/useQ_[Scope]_Profiles.ts
import { useQuery } from "@tanstack/react-query";
import { useMemo } from "react";
import { supabase, QueryData } from "@/configs/supabase/config";
import { QueryKeys } from "@/utils/query/queryKeys";
import useApp from "antd/es/app/useApp";
export type UseQ_[Scope]_Profiles_Params = {
userIds: string[];
enabled?: boolean;
};
const createProfilesQuery = (userIds: string[]) =>
supabase
.from("profiles")
.select(`
id,
email,
whitelisted,
whitelisted_at,
created_at,
updated_at
`)
.in("id", userIds);
export type [Scope]_Profiles_QueryData = QueryData<ReturnType<typeof createProfilesQuery>>;
export type [Scope]_Profile = [Scope]_Profiles_QueryData[number];
export const useQ_[Scope]_Profiles = (params: UseQ_[Scope]_Profiles_Params) => {
const { message } = useApp();
const { userIds, enabled = true } = params;
const query = useQuery({
enabled: enabled && userIds.length > 0,
queryKey: [...QueryKeys.profiles.all(), { userIds: [...userIds].sort() }] as const,
queryFn: async () => {
if (userIds.length === 0) return [];
const sb_FromProfiles_Select = await createProfilesQuery(userIds);
if (sb_FromProfiles_Select.error) {
console.error(sb_FromProfiles_Select.error);
message.error("Failed to fetch user profiles!");
throw sb_FromProfiles_Select.error;
}
return sb_FromProfiles_Select.data;
},
});
const profiles = useMemo(() => query.data || [], [query.data]);
const profilesMap = useMemo(
() =>
profiles.reduce(
(acc, profile) => {
acc[profile.id] = profile;
return acc;
},
{} as Record<string, [Scope]_Profile>
),
[profiles]
);
return {
query,
profiles,
profilesMap,
};
};
import { useMemo } from "react";
import { Table } from "antd";
import { useQ_[Scope]_ParentData } from "@/hooks/...";
import { useQ_[Scope]_Profiles } from "@/hooks/...";
function MyComponent() {
// Step 1: Fetch parent data with user_ids
const qParent = useQ_[Scope]_ParentData({ ... });
// Step 2: Extract user IDs
const userIds = useMemo(
() => qParent.items.map(item => item.user_id),
[qParent.items]
);
// Step 3: Batch fetch profiles
const qProfiles = useQ_[Scope]_Profiles({
userIds,
enabled: !qParent.query.isLoading
});
// Step 4: Loading states
if (qParent.query.isLoading || qProfiles.query.isLoading) {
return <Spin />;
}
if (qParent.query.error || qProfiles.query.error) {
return <Alert type="error" message="Failed to load data" />;
}
// Step 5: Use profilesMap for O(1) lookups
return (
<Table
dataSource={qParent.items}
columns={[
{
title: "Email",
render: (_, record) => {
const profile = qProfiles.profilesMap[record.user_id];
return <span>{profile?.email || "Loading..."}</span>;
},
},
]}
/>
);
}
const qProfiles = useQ_[Scope]_Profiles({
userIds,
enabled: !qParent.query.isLoading // Wait for parent to load
});
Why: Prevents firing query before userIds array is populated
queryKey: [...QueryKeys.profiles.all(), { userIds: [...userIds].sort() }];
Why: Same set of user IDs (different order) should hit same cache
if (userIds.length === 0) return [];
Why: Prevents invalid .in("id", []) query to database
const profilesMap = useMemo(
() =>
profiles.reduce(
(acc, profile) => ({ ...acc, [profile.id]: profile }),
{} as Record<string, Profile>
),
[profiles]
);
Why: O(1) lookup performance in renders instead of O(n) with .find()
Hook: src/hooks/PageOrganization/PageOrganization_SettingsUsers/useQ_PageOrganizationSettingsUsers_Profiles.ts
Component: src/pages/Page_Organization/PageOrganization_SettingsUsers.tsx
useQ_[Scope]_Profiles (plural){ userIds: string[], enabled?: boolean }.in("id", userIds) query{ query, profiles, profilesMap }Use when deploying Cloudflare Workers, managing R2 storage, or working with Cloudflare infrastructure
Use when working with ANTD components, theme tokens, icons, forms, or feedback components (message/notification/modal)
Use when adding, referencing, or serving static assets (images, fonts, videos, 3D models) through the R2 CDN pipeline with type-safe imports
Use when writing or reviewing JavaScript/TypeScript code for style patterns like concise arrows, inline handlers, expression formatting, or when tempted to use eslint-disable
Use when working with environment variables in frontend code
Use when creating or modifying keyboard shortcuts/hotkeys in frontend code