From 9190b5c97aa6ca3c7c355cac6a7417924bcdcc0f Mon Sep 17 00:00:00 2001 From: k-j-kim <17989954+k-j-kim@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:25:33 -0700 Subject: [PATCH] feat: consolidate webapp data skills into using-webapp-salesforce-data @W-21338965@ (#81) feat: using-webapp-salesforce-data skill and skill validation updates Consolidate webapp data access guidance into using-webapp-salesforce-data. Add js-yaml-based frontmatter parsing and plain-scalar description checks in validate-skills. Update dependent skills, samples, and package metadata. Made-with: Cursor --- skills/accessing-webapp-data/SKILL.md | 178 --- .../SKILL.md | 2 +- .../building-webapp-react-components/SKILL.md | 2 +- .../SKILL.md | 2 +- skills/configuring-webapp-metadata/SKILL.md | 4 +- skills/creating-webapp/SKILL.md | 5 +- .../deploying-webapp-to-salesforce/SKILL.md | 7 +- .../exploring-webapp-graphql-schema/SKILL.md | 149 --- skills/fetching-webapp-rest-api/SKILL.md | 167 --- skills/generating-custom-application/SKILL.md | 2 +- skills/generating-custom-field/SKILL.md | 2 +- .../generating-custom-lightning-type/SKILL.md | 2 +- skills/generating-custom-object/SKILL.md | 2 +- skills/generating-custom-tab/SKILL.md | 2 +- .../generating-experience-lwr-site/SKILL.md | 2 +- .../generating-experience-react-site/SKILL.md | 2 +- skills/generating-flexipage/SKILL.md | 2 +- skills/generating-flow/SKILL.md | 2 +- skills/generating-fragment/SKILL.md | 2 +- skills/generating-list-view/SKILL.md | 2 +- skills/generating-permission-set/SKILL.md | 2 +- skills/generating-validation-rule/SKILL.md | 2 +- .../SKILL.md | 258 ---- .../SKILL.md | 253 ---- .../implementing-webapp-file-upload/SKILL.md | 2 +- skills/installing-webapp-features/SKILL.md | 2 +- .../SKILL.md | 2 +- skills/trigger-refactor-pipeline/SKILL.md | 2 +- skills/using-webapp-graphql/SKILL.md | 324 ----- .../shared-schema.graphqls | 1150 ----------------- skills/using-webapp-salesforce-data/SKILL.md | 363 ++++++ .../graphql-search.sh | 139 ++ 32 files changed, 528 insertions(+), 2509 deletions(-) delete mode 100644 skills/accessing-webapp-data/SKILL.md delete mode 100644 skills/exploring-webapp-graphql-schema/SKILL.md delete mode 100644 skills/fetching-webapp-rest-api/SKILL.md delete mode 100644 skills/generating-webapp-graphql-mutation-query/SKILL.md delete mode 100644 skills/generating-webapp-graphql-read-query/SKILL.md delete mode 100644 skills/using-webapp-graphql/SKILL.md delete mode 100644 skills/using-webapp-graphql/shared-schema.graphqls create mode 100644 skills/using-webapp-salesforce-data/SKILL.md create mode 100644 skills/using-webapp-salesforce-data/graphql-search.sh diff --git a/skills/accessing-webapp-data/SKILL.md b/skills/accessing-webapp-data/SKILL.md deleted file mode 100644 index e862745..0000000 --- a/skills/accessing-webapp-data/SKILL.md +++ /dev/null @@ -1,178 +0,0 @@ ---- -name: accessing-webapp-data -description: Salesforce data access patterns. Use when adding or modifying any code that fetches data from Salesforce (records, Chatter, Connect API, etc.). -paths: - - "**/*.ts" - - "**/*.tsx" - - "**/*.graphql" ---- - -# Salesforce Data Access - -Guidance for accessing Salesforce data from web apps. **All Salesforce data fetches MUST use the Data SDK** (`@salesforce/sdk-data`). The SDK provides authentication, CSRF handling, and correct base URL resolution — direct `fetch` or `axios` calls bypass these and are not allowed. - -## Mandatory: Use the Data SDK - -> **Every Salesforce data fetch must go through the Data SDK.** Obtain it via `createDataSDK()`, then use `sdk.graphql?.()` or `sdk.fetch?.()`. Never call `fetch()` or `axios` directly for Salesforce endpoints. - -## Optional Chaining and Graceful Handling - -**Always use optional chaining** when calling `sdk.graphql` or `sdk.fetch` — these methods may be undefined in some surfaces (e.g., Salesforce ACC, MCP Apps). Handle the case where they are not available gracefully: - -```typescript -const sdk = await createDataSDK(); - -// ✅ Use optional chaining -const response = await sdk.graphql?.(query); - -// ✅ Check before using fetch -if (!sdk.fetch) { - throw new Error("Data SDK fetch is not available in this context"); -} -const res = await sdk.fetch(url); -``` - -For GraphQL, if `sdk.graphql` is undefined, the call returns `undefined` — handle that in your logic (e.g., throw a clear error or return a fallback). For `sdk.fetch`, check availability before calling when the operation is required. - -## Preference: GraphQL First - -**GraphQL is the preferred method** for querying and mutating Salesforce records. Use it when: - -- Querying records (Account, Contact, Opportunity, custom objects) -- Creating, updating, or deleting records (when GraphQL supports the operation) -- Fetching related data, filters, sorting, pagination - -**Use `sdk.fetch` only when GraphQL is not sufficient.** For REST API usage, invoke the `fetching-rest-api` skill, which documents: - -- Chatter API (e.g., `/services/data/v65.0/chatter/users/me`) -- Connect REST API (e.g., `/services/data/v65.0/connect/file/upload/config`) -- Apex REST (e.g., `/services/apexrest/auth/login`) -- UI API REST (e.g., `/services/data/v65.0/ui-api/records/{recordId}`) -- Einstein LLM Gateway - ---- - -## Getting the SDK - -```typescript -import { createDataSDK } from "@salesforce/sdk-data"; - -const sdk = await createDataSDK(); -``` - ---- - -## Example 1: GraphQL (Preferred) - -For record queries and mutations, use GraphQL via the Data SDK. Invoke the `using-graphql` skill for the full workflow (schema exploration, query authoring, codegen, lint validate). - -```typescript -import { createDataSDK, gql } from "@salesforce/sdk-data"; -import type { GetAccountsQuery } from "../graphql-operations-types"; - -const GET_ACCOUNTS = gql` - query GetAccounts { - uiapi { - query { - Account(first: 10) { - edges { - node { - Id - Name { value } - } - } - } - } - } - } -`; - -export async function getAccounts() { - const sdk = await createDataSDK(); - const response = await sdk.graphql?.(GET_ACCOUNTS); - - if (response?.errors?.length) { - throw new Error(response.errors.map((e) => e.message).join("; ")); - } - - return response?.data?.uiapi?.query?.Account?.edges?.map((e) => e?.node) ?? []; -} -``` - ---- - -## Example 2: Fetch (When GraphQL Is Not Sufficient) - -For REST endpoints that have no GraphQL equivalent, use `sdk.fetch`. **Invoke the `fetching-rest-api` skill** for full documentation of Chatter, Connect REST, Apex REST, UI API REST, and Einstein LLM endpoints. - -```typescript -import { createDataSDK } from "@salesforce/sdk-data"; - -declare const __SF_API_VERSION__: string; -const API_VERSION = typeof __SF_API_VERSION__ !== "undefined" ? __SF_API_VERSION__ : "65.0"; - -export async function getCurrentUser() { - const sdk = await createDataSDK(); - const response = await sdk.fetch?.(`/services/data/v${API_VERSION}/chatter/users/me`); - - if (!response?.ok) throw new Error(`HTTP ${response?.status}`); - const data = await response.json(); - return { id: data.id, name: data.name }; -} -``` - ---- - -## Anti-Patterns (Forbidden) - -### Direct fetch to Salesforce - -```typescript -// ❌ FORBIDDEN — bypasses Data SDK auth and CSRF -const res = await fetch("/services/data/v65.0/chatter/users/me"); -``` - -### Direct axios to Salesforce - -```typescript -// ❌ FORBIDDEN — bypasses Data SDK -const res = await axios.get("/services/data/v65.0/chatter/users/me"); -``` - -### Correct approach - -```typescript -// ✅ CORRECT — use Data SDK -const sdk = await createDataSDK(); -const res = await sdk.fetch?.("/services/data/v65.0/chatter/users/me"); -``` - ---- - -## Clarifying Vague Data Requests - -When a user asks about data and the request is vague, **clarify before implementing**. Ask which of the following they want: - -- **Application code** — Add or modify code in a specific web app so the app performs the data interaction at runtime (e.g., GraphQL query in the React app) -- **Local SF CLI** — Run Salesforce CLI commands locally (e.g., `sf data query`, `sf data import tree`) to interact with the org from the terminal -- **Local example data** — Update or add local fixture/example data files (e.g., JSON in `data/`) for development or testing -- **Other** — Data export, report generation, setup script, etc. - -Do not assume. A request like "fetch accounts" could mean: (1) add a GraphQL query to the app, (2) run `sf data query` in the terminal, or (3) update sample data files. Confirm the intent before proceeding. - ---- - -## Decision Flow - -1. **Need to query or mutate Salesforce records?** → Use GraphQL via the Data SDK. Invoke the `using-graphql` skill. -2. **Need Chatter, Connect REST, Apex REST, UI API REST, or Einstein LLM?** → Use `sdk.fetch`. Invoke the `fetching-rest-api` skill. -3. **Never** use `fetch`, `axios`, or similar directly for Salesforce API calls. - ---- - -## Reference - -- GraphQL workflow: invoke the `using-graphql` skill (`.a4drules/skills/using-graphql/`) -- REST API via fetch: invoke the `fetching-rest-api` skill (`.a4drules/skills/fetching-rest-api/`) -- Data SDK package: `@salesforce/sdk-data` (`createDataSDK`, `gql`, `NodeOfConnection`) -- `createRecord` for UI API record creation: `@salesforce/webapp-experimental/api` (uses Data SDK internally) diff --git a/skills/building-webapp-data-visualization/SKILL.md b/skills/building-webapp-data-visualization/SKILL.md index 59978d1..1d67b5e 100644 --- a/skills/building-webapp-data-visualization/SKILL.md +++ b/skills/building-webapp-data-visualization/SKILL.md @@ -1,6 +1,6 @@ --- name: building-webapp-data-visualization -description: Adds data visualization components (charts, stat cards, KPI metrics) to React pages using Recharts. Use when the user asks to add a chart, graph, donut chart, pie chart, bar chart, stat card, KPI metric, dashboard visualization, or analytics component to the web application. +description: "Adds data visualization components (charts, stat cards, KPI metrics) to React pages using Recharts. Use when the user asks to add a chart, graph, donut chart, pie chart, bar chart, stat card, KPI metric, dashboard visualization, or analytics component to the web application." --- # Data Visualization diff --git a/skills/building-webapp-react-components/SKILL.md b/skills/building-webapp-react-components/SKILL.md index 8448424..3075988 100644 --- a/skills/building-webapp-react-components/SKILL.md +++ b/skills/building-webapp-react-components/SKILL.md @@ -1,6 +1,6 @@ --- name: building-webapp-react-components -description: Use when editing any React code in the web application — creating or modifying components, pages, layout, headers, footers, or any TSX/JSX files. Follow this skill for add component, add page, header/footer, and general React UI implementation patterns (shadcn UI and Tailwind CSS). +description: "Use when editing any React code in the web application — creating or modifying components, pages, layout, headers, footers, or any TSX/JSX files. Follow this skill for add component, add page, header/footer, and general React UI implementation patterns (shadcn UI and Tailwind CSS)." --- # React Web App (Components, Pages, Layout) diff --git a/skills/configuring-webapp-csp-trusted-sites/SKILL.md b/skills/configuring-webapp-csp-trusted-sites/SKILL.md index fc01a12..29b69db 100644 --- a/skills/configuring-webapp-csp-trusted-sites/SKILL.md +++ b/skills/configuring-webapp-csp-trusted-sites/SKILL.md @@ -1,6 +1,6 @@ --- name: configuring-webapp-csp-trusted-sites -description: Creates Salesforce CSP Trusted Site metadata when adding external domains. Use when the user adds an external API, CDN, image host, font provider, map tile server, or any third-party URL that the web application needs to load resources from — or when a browser console shows a CSP violation error. +description: "Creates Salesforce CSP Trusted Site metadata when adding external domains. Use when the user adds an external API, CDN, image host, font provider, map tile server, or any third-party URL that the web application needs to load resources from — or when a browser console shows a CSP violation error." --- # CSP Trusted Sites diff --git a/skills/configuring-webapp-metadata/SKILL.md b/skills/configuring-webapp-metadata/SKILL.md index 0fb714e..2410b84 100644 --- a/skills/configuring-webapp-metadata/SKILL.md +++ b/skills/configuring-webapp-metadata/SKILL.md @@ -1,6 +1,6 @@ --- name: configuring-webapp-metadata -description: Use this skill when configuring web application metadata structure, webapplication.json, or bundle organization. Covers WebApplication bundle layout, meta XML, build output directory, and webapplication.json settings. +description: "Use this skill when configuring web application metadata structure, webapplication.json, or bundle organization. Covers WebApplication bundle layout, meta XML, build output directory, and webapplication.json settings." --- # WebApplication Requirements @@ -53,7 +53,7 @@ Applies to: Reject: - backslashes -- leading `\` +- leading `/` or `\` - `..` segments - null or control characters - globs: `*`, `?`, `**` diff --git a/skills/creating-webapp/SKILL.md b/skills/creating-webapp/SKILL.md index b6f1ab6..d7b8b9c 100644 --- a/skills/creating-webapp/SKILL.md +++ b/skills/creating-webapp/SKILL.md @@ -1,6 +1,6 @@ --- name: creating-webapp -description: Use this skill when creating or setting up a new SFDX React web application. Covers first steps, npm install, skills-first protocol, deployment order, and core web app rules. +description: "Use this skill when creating or setting up a new SFDX React web application. Covers first steps, npm install, skills-first protocol, deployment order, and core web app rules." paths: - "**/webapplications/**/*" --- @@ -111,7 +111,7 @@ Apps run behind dynamic base paths. Router navigation (``, `navigate()` ## Module Restrictions -React apps must NOT import Salesforce platform modules like `lightning/*` or `@wire` (LWC-only). For data access, invoke the **accessing-data** skill. +React apps must NOT import Salesforce platform modules like `lightning/*` or `@wire` (LWC-only). For data access, invoke the **using-salesforce-data** skill. # Frontend Aesthetics @@ -138,4 +138,3 @@ Only stop when: - All checklist items are completed and quality gates pass, or - A blocking error cannot be resolved after reasonable remediation, or - The user explicitly asks to pause. - diff --git a/skills/deploying-webapp-to-salesforce/SKILL.md b/skills/deploying-webapp-to-salesforce/SKILL.md index de31253..16067db 100644 --- a/skills/deploying-webapp-to-salesforce/SKILL.md +++ b/skills/deploying-webapp-to-salesforce/SKILL.md @@ -1,8 +1,6 @@ --- name: deploying-webapp-to-salesforce -description: Enforces the correct order for deploying metadata, assigning permission sets, and fetching GraphQL schema. Use for ANY deployment to a Salesforce org — webapps, LWC, Aura, Apex, metadata, schema fetch, or org sync. Codifies setup-cli.mjs. -paths: - - "**/*" +description: "Enforces the correct order for deploying metadata, assigning permission sets, and fetching GraphQL schema. Use for ANY deployment to a Salesforce org — webapps, LWC, Aura, Apex, metadata, schema fetch, or org sync. Codifies setup-cli.mjs." --- # Deploying to Salesforce @@ -225,5 +223,4 @@ The project includes `scripts/setup-cli.mjs` which runs this sequence in batch. ## Related Skills -- **exploring-graphql-schema** — Schema exploration (grep-only) after schema exists -- **using-graphql** — Full GraphQL workflow (explore, query, codegen, lint) +- **using-salesforce-data** — Full data access workflow (GraphQL queries/mutations, REST APIs, schema exploration, webapp integration) diff --git a/skills/exploring-webapp-graphql-schema/SKILL.md b/skills/exploring-webapp-graphql-schema/SKILL.md deleted file mode 100644 index 6b7578d..0000000 --- a/skills/exploring-webapp-graphql-schema/SKILL.md +++ /dev/null @@ -1,149 +0,0 @@ ---- -name: exploring-webapp-graphql-schema -description: Explore the Salesforce GraphQL schema via grep-only lookups. Use before generating any GraphQL query — schema exploration must complete first. -paths: - - "**/*.ts" - - "**/*.tsx" - - "**/*.graphql" ---- - -# Salesforce GraphQL Schema Exploration - -Guidance for AI agents working with the Salesforce GraphQL API schema. **GREP ONLY** — the schema file is very large (~265,000+ lines). All lookups MUST use grep; do NOT open, read, stream, or parse the file. - -## Deployment Prerequisites - -The schema reflects the **current org state**. Custom objects and fields appear only after metadata is deployed. - -- **Before** running `npm run graphql:schema`: Deploy all metadata (objects, permission sets, layouts) and assign the permission set to the target user. Invoke the `deploying-to-salesforce` skill for the full sequence. -- **After** any metadata deployment: Re-run `npm run graphql:schema` and `npm run graphql:codegen` so types and queries stay in sync. - -## Schema File Location - -**Location:** `schema.graphql` at the **SFDX project root** (NOT inside the webapp dir). All grep commands **must be run from the project root** where `schema.graphql` lives. - -> ⚠️ **Important (Access Policy - GREP ONLY)**: Do NOT open, view, stream, paginate, or parse the schema with any tool other than grep. All lookups MUST be done via grep using anchored patterns with minimal context as defined below. - -If the file is not present, generate it by running (from the **webapp dir**, not the project root): - -```bash -# Run from webapp dir (force-app/main/default/webapplications//) -npm run graphql:schema -``` - -**BEFORE generating any GraphQL query, you MUST:** - -1. **Check if schema exists**: Look for `schema.graphql` in the **SFDX project root** -2. **If schema is missing**: - - `cd` to the **webapp dir** and run `npm run graphql:schema` to download it - - Wait for the command to complete successfully - - Then proceed with grep-only lookups as defined below. -3. **If schema exists**: Proceed with targeted searches as described below - -> ⚠️ **DO NOT** generate GraphQL queries without first having access to the schema. Standard field assumptions may not match the target org's configuration. - -## Schema Structure Overview - -Main entry points: `Query { uiapi }` for reads; `Mutation { uiapi(input: ...) }` for creates/updates/deletes. Record queries use `uiapi.query.`. - -## Allowed Lookups (grep-only) - -Use ONLY these grep commands to locate specific definitions in schema.graphql. Do not use editors (VS Code/vim/nano), cat/less/more/head/tail, or programmatic parsers (node/python/awk/sed/jq). - -- Always include: - - `-n` (line numbers) and `-E` (extended regex) - - Anchors (`^`) and word boundaries (`\b`) - - Minimal context with `-A N` (prefer the smallest N that surfaces the needed lines) - -### 1. Find Available Fields for a Record Type - -Search for `type implements Record` to find all queryable fields: - -```bash -# Example: Find Account fields (anchored, minimal context) -grep -nE '^type[[:space:]]+Account[[:space:]]+implements[[:space:]]+Record\b' ./schema.graphql -A 60 -``` - -### 2. Find Filter Options for a Record Type - -Search for `input _Filter` to find filterable fields and operators: - -```bash -# Example: Find Account filter options (anchored) -grep -nE '^input[[:space:]]+Account_Filter\b' ./schema.graphql -A 40 -``` - -### 3. Find OrderBy Options - -Search for `input _OrderBy` for sorting options: - -```bash -# Example: Find Account ordering options (anchored) -grep -nE '^input[[:space:]]+Account_OrderBy\b' ./schema.graphql -A 30 -``` - -### 4. Find Mutation Operations - -Search for operations in `UIAPIMutations`: - -```bash -# Example: Find Account mutations (extended regex) -grep -nE 'Account.*(Create|Update|Delete)' ./schema.graphql -``` - -### 5. Find Input Types for Mutations - -Search for `input CreateInput` or `input UpdateInput`: - -```bash -# Example: Find Account create input (anchored) -grep -nE '^input[[:space:]]+AccountCreateInput\b' ./schema.graphql -A 30 -``` - -## Agent Workflow for Building Queries (grep-only) - -**Pre-requisites (MANDATORY):** - -- [ ] Verified `schema.graphql` exists in the **SFDX project root** -- [ ] If missing, ran `npm run graphql:schema` from the **webapp dir** and waited for completion -- [ ] Confirmed connection to correct Salesforce org (if downloading fresh schema) - -**Workflow Steps:** - -1. **Identify the target object** (e.g., Account, Contact, Opportunity) -2. **Run the "Find Available Fields" grep command** for your object (copy only the field names visible in the grep output; do not open the file) -3. **Run the "Find Filter Options" grep command** (`_Filter`) to understand filtering options -4. **Run the "Find OrderBy Options" grep command** (`_OrderBy`) for sorting capabilities -5. **Build the query** following the patterns in the `generating-graphql-read-query` or `generating-graphql-mutation-query` skill using only values returned by grep -6. **Validate field names** using grep matches (case-sensitive). Do not open or parse the file beyond grep. - -## Tips for Agents - -- **Always verify field names** by running the specific grep commands; do not open the schema file -- **Use grep with anchors and minimal -A context** to explore the schema efficiently—never read or stream the file -- **Check relationships** by looking for `parentRelationship` and `childRelationship` comments in type definitions -- **Look for Connection types** (e.g., `AccountConnection`) via grep to understand pagination structure -- **Custom objects** end with `__c` (e.g., `CustomObject__c`) -- **Custom fields** also end with `__c` (e.g., `Custom_Field__c`) - -## Forbidden Operations - -To prevent accidental large reads, the following are prohibited for schema.graphql: - -- Opening in any editor (VS Code, vim, nano) -- Using cat, less, more, head, or tail -- Programmatic parsing (node, python, awk, sed, jq) -- Streaming or paginating through large portions of the file - -If any of the above occurs, stop and replace the action with one of the Allowed Lookups (grep-only). - -## Output Minimization - -- Prefer precise, anchored patterns with word boundaries -- Use the smallest `-A` context that surfaces required lines -- If results are noisy, refine the regex rather than increasing context - -## Related Skills - -- For generating read queries, invoke the `generating-graphql-read-query` skill -- For generating mutation queries, invoke the `generating-graphql-mutation-query` skill diff --git a/skills/fetching-webapp-rest-api/SKILL.md b/skills/fetching-webapp-rest-api/SKILL.md deleted file mode 100644 index 8270c27..0000000 --- a/skills/fetching-webapp-rest-api/SKILL.md +++ /dev/null @@ -1,167 +0,0 @@ ---- -name: fetching-webapp-rest-api -description: REST API usage via the Data SDK fetch method. Use when implementing Chatter, Connect REST, Apex REST, UI API REST, or Einstein LLM calls — only when GraphQL is not sufficient. -paths: - - "**/*.ts" - - "**/*.tsx" - - "**/*.graphql" ---- - -# Salesforce REST API via Data SDK Fetch - -Use `sdk.fetch` from the Data SDK when GraphQL is not sufficient. The SDK applies authentication, CSRF handling, and base URL resolution. **Always use optional chaining** (`sdk.fetch?.()`) and handle the case where `fetch` is not available. - -Invoke this skill when you need to call Chatter, Connect REST, Apex REST, UI API REST, or Einstein LLM endpoints. - -## API Version - -Use the project's API version. It is typically injected as `__SF_API_VERSION__`; fallback to `"65.0"`: - -```typescript -declare const __SF_API_VERSION__: string; -const API_VERSION = typeof __SF_API_VERSION__ !== "undefined" ? __SF_API_VERSION__ : "65.0"; -``` - -## Base Path - -URLs are relative to the Salesforce API base. The SDK prepends the correct base path. Use paths starting with `/services/...`. - ---- - -## Chatter API - -User and collaboration data. No GraphQL equivalent. - -| Endpoint | Method | Purpose | -| -------- | ------ | ------- | -| `/services/data/v{version}/chatter/users/me` | GET | Current user (id, name, email, username) | - -```typescript -const sdk = await createDataSDK(); -const response = await sdk.fetch?.(`/services/data/v${API_VERSION}/chatter/users/me`); - -if (!response?.ok) throw new Error(`HTTP ${response?.status}`); -const data = await response.json(); -return { id: data.id, name: data.name }; -``` - ---- - -## Connect REST API - -File and content operations. - -| Endpoint | Method | Purpose | -| -------- | ------ | ------- | -| `/services/data/v{version}/connect/file/upload/config` | GET | Upload config (token, uploadUrl) for file uploads | - -```typescript -const sdk = await createDataSDK(); -const configRes = await sdk.fetch?.(`/services/data/v${API_VERSION}/connect/file/upload/config`, { - method: "GET", -}); - -if (!configRes?.ok) throw new Error(`Failed to get upload config: ${configRes?.status}`); -const config = await configRes.json(); -const { token, uploadUrl } = config; -``` - ---- - -## Apex REST - -Custom Apex REST resources. Requires corresponding Apex classes in the org. CSRF protection is applied automatically for `services/apexrest` URLs. - -| Endpoint | Method | Purpose | -| -------- | ------ | ------- | -| `/services/apexrest/auth/login` | POST | User login | -| `/services/apexrest/auth/register` | POST | User registration | -| `/services/apexrest/auth/forgot-password` | POST | Request password reset | -| `/services/apexrest/auth/reset-password` | POST | Reset password with token | -| `/services/apexrest/auth/change-password` | POST | Change password (authenticated) | -| `/services/apexrest/{resource}` | GET/POST | Custom Apex REST resources | - -**Example (login):** - -```typescript -const sdk = await createDataSDK(); -const response = await sdk.fetch?.("/services/apexrest/auth/login", { - method: "POST", - body: JSON.stringify({ email, password, startUrl: "/" }), - headers: { "Content-Type": "application/json", Accept: "application/json" }, -}); -``` - -Apex REST paths do not include the API version. - ---- - -## UI API (REST) - -When GraphQL cannot cover the use case. **Prefer GraphQL** when possible. - -| Endpoint | Method | Purpose | -| -------- | ------ | ------- | -| `/services/data/v{version}/ui-api/records/{recordId}` | GET | Fetch a single record | - -```typescript -const sdk = await createDataSDK(); -const response = await sdk.fetch?.(`/services/data/v${API_VERSION}/ui-api/records/${recordId}`); -``` - ---- - -## Einstein LLM Gateway - -AI features. Requires Einstein API setup. - -| Endpoint | Method | Purpose | -| -------- | ------ | ------- | -| `/services/data/v{version}/einstein/llm/prompt/generations` | POST | Generate text from Einstein LLM | - -```typescript -const sdk = await createDataSDK(); -const response = await sdk.fetch?.(`/services/data/v${API_VERSION}/einstein/llm/prompt/generations`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - additionalConfig: { applicationName: "PromptTemplateGenerationsInvocable" }, - promptTextorId: prompt, - }), -}); - -if (!response?.ok) throw new Error(`Einstein LLM failed (${response?.status})`); -const data = await response.json(); -return data?.generations?.[0]?.text ?? ""; -``` - ---- - -## General Pattern - -```typescript -import { createDataSDK } from "@salesforce/sdk-data"; - -const sdk = await createDataSDK(); - -if (!sdk.fetch) { - throw new Error("Data SDK fetch is not available in this context"); -} - -const response = await sdk.fetch(url, { - method: "GET", // or POST, PUT, PATCH, DELETE - headers: { "Content-Type": "application/json", Accept: "application/json" }, - body: method !== "GET" ? JSON.stringify(payload) : undefined, -}); - -if (!response.ok) throw new Error(`HTTP ${response.status}`); -const data = await response.json(); -``` - ---- - -## Reference - -- Parent: `accessing-data` — enforces Data SDK usage for all Salesforce data fetches -- GraphQL: `using-graphql` — use for record queries and mutations when possible -- `createRecord` from `@salesforce/webapp-experimental/api` for UI API record creation (uses SDK internally) diff --git a/skills/generating-custom-application/SKILL.md b/skills/generating-custom-application/SKILL.md index ece9b0b..7d9e75b 100644 --- a/skills/generating-custom-application/SKILL.md +++ b/skills/generating-custom-application/SKILL.md @@ -1,6 +1,6 @@ --- name: generating-custom-application -description: Use this skill when users need to create or configure Salesforce Custom Applications. Trigger when users mention custom apps, application metadata, app navigation, or organizing tabs into applications. Use when users want to create app containers for tabs and pages. Always use this skill for custom application work. +description: "Use this skill when users need to create or configure Salesforce Custom Applications. Trigger when users mention custom apps, application metadata, app navigation, or organizing tabs into applications. Use when users want to create app containers for tabs and pages. Always use this skill for custom application work." --- ## When to Use This Skill diff --git a/skills/generating-custom-field/SKILL.md b/skills/generating-custom-field/SKILL.md index 41d8d8a..6eb9c6f 100644 --- a/skills/generating-custom-field/SKILL.md +++ b/skills/generating-custom-field/SKILL.md @@ -1,6 +1,6 @@ --- name: generating-custom-field -description: Use this skill when users need to create, generate, or validate Salesforce Custom Field metadata. Trigger when users mention custom fields, field types, Roll-up Summary fields, Master-Detail relationships, Lookup relationships, formula fields, picklists, or field metadata. Also use when users encounter field deployment errors, especially around Roll-up Summary format, Master-Detail constraints, or formula issues. Always use this skill for any custom field metadata work, field generation, or field troubleshooting. +description: "Use this skill when users need to create, generate, or validate Salesforce Custom Field metadata. Trigger when users mention custom fields, field types, Roll-up Summary fields, Master-Detail relationships, Lookup relationships, formula fields, picklists, or field metadata. Also use when users encounter field deployment errors, especially around Roll-up Summary format, Master-Detail constraints, or formula issues. Always use this skill for any custom field metadata work, field generation, or field troubleshooting." --- ## When to Use This Skill diff --git a/skills/generating-custom-lightning-type/SKILL.md b/skills/generating-custom-lightning-type/SKILL.md index 2b89d15..0fded2b 100644 --- a/skills/generating-custom-lightning-type/SKILL.md +++ b/skills/generating-custom-lightning-type/SKILL.md @@ -1,6 +1,6 @@ --- name: generating-custom-lightning-type -description: Use this skill when users need to create Custom Lightning Types (CLTs) for Einstein Agent actions or structured input/output schemas. Trigger when users mention CLT, Custom Lightning Types, JSON schemas for agents, type definitions, lightning__objectType, or editor/renderer configurations. This is complex - always use this skill for CLT work. +description: "Use this skill when users need to create Custom Lightning Types (CLTs) for Einstein Agent actions or structured input/output schemas. Trigger when users mention CLT, Custom Lightning Types, JSON schemas for agents, type definitions, lightning__objectType, or editor/renderer configurations. This is complex - always use this skill for CLT work." --- ## When to Use This Skill diff --git a/skills/generating-custom-object/SKILL.md b/skills/generating-custom-object/SKILL.md index 581858b..c4ac93d 100644 --- a/skills/generating-custom-object/SKILL.md +++ b/skills/generating-custom-object/SKILL.md @@ -1,6 +1,6 @@ --- name: generating-custom-object -description: Use this skill when users need to create, generate, or validate Salesforce Custom Object metadata. Trigger when users mention custom objects, creating objects, object metadata, .object files, sharing models, name fields, or validation rules on objects. Also use when users say things like "create a custom object", "generate object metadata", "set up an object for...", or when they're troubleshooting object deployment errors especially around sharing models and Master-Detail relationships. Always use this skill for any custom object metadata work. +description: "Use this skill when users need to create, generate, or validate Salesforce Custom Object metadata. Trigger when users mention custom objects, creating objects, object metadata, .object files, sharing models, name fields, or validation rules on objects. Also use when users say things like \"create a custom object\", \"generate object metadata\", \"set up an object for...\", or when they're troubleshooting object deployment errors especially around sharing models and Master-Detail relationships. Always use this skill for any custom object metadata work." --- ## When to Use This Skill diff --git a/skills/generating-custom-tab/SKILL.md b/skills/generating-custom-tab/SKILL.md index f4f9577..648b5b8 100644 --- a/skills/generating-custom-tab/SKILL.md +++ b/skills/generating-custom-tab/SKILL.md @@ -1,6 +1,6 @@ --- name: generating-custom-tab -description: Use this skill when users need to create or configure Salesforce Custom Tabs. Trigger when users mention tabs, navigation tabs, object tabs, web tabs, Visualforce tabs, Lightning component tabs, app page tabs, or tab configuration. Also use when users want to add navigation to custom objects, create tabs for external content, or set up Lightning page tabs. Always use this skill for any custom tab work. +description: "Use this skill when users need to create or configure Salesforce Custom Tabs. Trigger when users mention tabs, navigation tabs, object tabs, web tabs, Visualforce tabs, Lightning component tabs, app page tabs, or tab configuration. Also use when users want to add navigation to custom objects, create tabs for external content, or set up Lightning page tabs. Always use this skill for any custom tab work." --- ## When to Use This Skill diff --git a/skills/generating-experience-lwr-site/SKILL.md b/skills/generating-experience-lwr-site/SKILL.md index 2cc2d80..6f626b3 100644 --- a/skills/generating-experience-lwr-site/SKILL.md +++ b/skills/generating-experience-lwr-site/SKILL.md @@ -1,6 +1,6 @@ --- name: generating-experience-lwr-site -description: Creates, modifies, or manages Salesforce Experience Cloud LWR sites via DigitalExperience metadata. Always trigger when users mention Experience sites, LWR sites, DigitalExperience, Experience Cloud, community sites, portals, creating pages, adding routes, views, theme layouts, branding sets, previewing sites, or any DigitalExperience bundle work. Also use when users mention specific content types like sfdc_cms__route, sfdc_cms__themeLayout, etc. or when troubleshooting site deployment. +description: "Creates, modifies, or manages Salesforce Experience Cloud LWR sites via DigitalExperience metadata. Always trigger when users mention Experience sites, LWR sites, DigitalExperience, Experience Cloud, community sites, portals, creating pages, adding routes, views, theme layouts, branding sets, previewing sites, or any DigitalExperience bundle work. Also use when users mention specific content types like sfdc_cms__route, sfdc_cms__themeLayout, etc. or when troubleshooting site deployment." --- # Experience LWR Site Builder diff --git a/skills/generating-experience-react-site/SKILL.md b/skills/generating-experience-react-site/SKILL.md index b20d485..4ab563d 100644 --- a/skills/generating-experience-react-site/SKILL.md +++ b/skills/generating-experience-react-site/SKILL.md @@ -1,6 +1,6 @@ --- name: generating-experience-react-site -description: Use this skill when users need to create or configure a Salesforce Digital Experience Site specifically for hosting a React web application. Trigger when users mention creating an Experience site for a React app, setting up a React site on Salesforce, configuring Network/CustomSite/DigitalExperience metadata for a web app, or deploying site infrastructure for a React application. Also trigger when users mention site URL path prefixes, app namespaces, appDevName, guest access configuration, DigitalExperienceConfig, DigitalExperienceBundle, or sfdc_cms__site content types in the context of React apps. Always use this skill for any React web application site creation or site infrastructure configuration work, even if the user just says "create a site for my React app" or "set up the site for my web application." +description: "Use this skill when users need to create or configure a Salesforce Digital Experience Site specifically for hosting a React web application. Trigger when users mention creating an Experience site for a React app, setting up a React site on Salesforce, configuring Network/CustomSite/DigitalExperience metadata for a web app, or deploying site infrastructure for a React application. Also trigger when users mention site URL path prefixes, app namespaces, appDevName, guest access configuration, DigitalExperienceConfig, DigitalExperienceBundle, or sfdc_cms__site content types in the context of React apps. Always use this skill for any React web application site creation or site infrastructure configuration work, even if the user just says \"create a site for my React app\" or \"set up the site for my web application.\"" --- # Digital Experience Site for React Web Applications diff --git a/skills/generating-flexipage/SKILL.md b/skills/generating-flexipage/SKILL.md index a8c0acc..b0c5c99 100644 --- a/skills/generating-flexipage/SKILL.md +++ b/skills/generating-flexipage/SKILL.md @@ -1,6 +1,6 @@ --- name: generating-flexipage -description: Use this skill when users need to create, generate, modify, or validate Salesforce Lightning pages (FlexiPages). Trigger when users mention RecordPage, AppPage, HomePage, Lightning pages, page layouts, adding components to pages, or page customization. Also use when users say things like "create a Lightning page", "add a component to a page", "customize the record page", "generate a FlexiPage", or when they're working with FlexiPage XML files and need help with components, regions, or deployment errors. Always use this skill for any FlexiPage-related work, even if they just mention "page" in the context of Salesforce. +description: "Use this skill when users need to create, generate, modify, or validate Salesforce Lightning pages (FlexiPages). Trigger when users mention RecordPage, AppPage, HomePage, Lightning pages, page layouts, adding components to pages, or page customization. Also use when users say things like \"create a Lightning page\", \"add a component to a page\", \"customize the record page\", \"generate a FlexiPage\", or when they're working with FlexiPage XML files and need help with components, regions, or deployment errors. Always use this skill for any FlexiPage-related work, even if they just mention \"page\" in the context of Salesforce." --- ## When to Use This Skill diff --git a/skills/generating-flow/SKILL.md b/skills/generating-flow/SKILL.md index 2f77bc2..8b14c97 100644 --- a/skills/generating-flow/SKILL.md +++ b/skills/generating-flow/SKILL.md @@ -1,6 +1,6 @@ --- name: generating-flow -description: Generate Salesforce Flows using the MCP tool execute_metadata_action. Use when the user asks to create, build, or generate a flow — including Screen, Autolaunched, Record-Triggered (before/after-save), Scheduled. Also trigger for flow-like requests such as "when a record is created", "trigger daily at", "send an email when", "update the field when", "automate", "workflow", or "flow XML/metadata". This is the only skill for Salesforce Flow generation. +description: "Generate Salesforce Flows using the MCP tool execute_metadata_action. Use when the user asks to create, build, or generate a flow — including Screen, Autolaunched, Record-Triggered (before/after-save), Scheduled. Also trigger for flow-like requests such as \"when a record is created\", \"trigger daily at\", \"send an email when\", \"update the field when\", \"automate\", \"workflow\", or \"flow XML/metadata\". This is the only skill for Salesforce Flow generation." --- ## Goal diff --git a/skills/generating-fragment/SKILL.md b/skills/generating-fragment/SKILL.md index 641cf1d..084208e 100644 --- a/skills/generating-fragment/SKILL.md +++ b/skills/generating-fragment/SKILL.md @@ -1,6 +1,6 @@ --- name: generating-fragment -description: Use this skill when users need to create or edit Salesforce Fragments (reusable UI pieces). Trigger when users mention fragments, UEM blocks, reusable UI templates, structured rendering across Slack/Mobile/LEX, or block-based layouts. Also use when users want to create unified experience components. Always use this skill for any fragment work. +description: "Use this skill when users need to create or edit Salesforce Fragments (reusable UI pieces). Trigger when users mention fragments, UEM blocks, reusable UI templates, structured rendering across Slack/Mobile/LEX, or block-based layouts. Also use when users want to create unified experience components. Always use this skill for any fragment work." --- ## When to Use This Skill diff --git a/skills/generating-list-view/SKILL.md b/skills/generating-list-view/SKILL.md index 78e2c1c..0e9391d 100644 --- a/skills/generating-list-view/SKILL.md +++ b/skills/generating-list-view/SKILL.md @@ -1,6 +1,6 @@ --- name: generating-list-view -description: Use this skill when users need to create, generate, or validate Salesforce List View metadata. Trigger when users mention list views, filtered record lists, creating views, setting up record columns, filtering records by criteria, or ask about list view visibility. Also use when users say things like "I need a view that shows...", "filter records by...", "create a list view for...", or when they're working with ListView XML files and need validation or troubleshooting. +description: "Use this skill when users need to create, generate, or validate Salesforce List View metadata. Trigger when users mention list views, filtered record lists, creating views, setting up record columns, filtering records by criteria, or ask about list view visibility. Also use when users say things like \"I need a view that shows...\", \"filter records by...\", \"create a list view for...\", or when they're working with ListView XML files and need validation or troubleshooting." --- ## When to Use This Skill diff --git a/skills/generating-permission-set/SKILL.md b/skills/generating-permission-set/SKILL.md index 69697ac..d7d79f4 100644 --- a/skills/generating-permission-set/SKILL.md +++ b/skills/generating-permission-set/SKILL.md @@ -1,6 +1,6 @@ --- name: generating-permission-set -description: Generates correct, deployable Salesforce permission set metadata (PermissionSet XML) with object, field, user, and app permissions. Use this skill when creating or editing permission set metadata, object permissions, field-level security (FLS), tab visibility, or deploying permission sets. +description: "Generates correct, deployable Salesforce permission set metadata (PermissionSet XML) with object, field, user, and app permissions. Use this skill when creating or editing permission set metadata, object permissions, field-level security (FLS), tab visibility, or deploying permission sets." compatibility: Salesforce Metadata API v60.0+ metadata: author: afv-library diff --git a/skills/generating-validation-rule/SKILL.md b/skills/generating-validation-rule/SKILL.md index 5698316..3a1e892 100644 --- a/skills/generating-validation-rule/SKILL.md +++ b/skills/generating-validation-rule/SKILL.md @@ -1,6 +1,6 @@ --- name: generating-validation-rule -description: Use this skill when users need to create, modify, or validate Salesforce Validation Rules. Trigger when users mention validation rules, field validation, data quality rules, formula validation, error messages, or validation logic. Also use when users encounter validation errors, need to update formulas, or want to enforce business rules at the data layer. Always use this skill for any validation rule work. +description: "Use this skill when users need to create, modify, or validate Salesforce Validation Rules. Trigger when users mention validation rules, field validation, data quality rules, formula validation, error messages, or validation logic. Also use when users encounter validation errors, need to update formulas, or want to enforce business rules at the data layer. Always use this skill for any validation rule work." --- ## When to Use This Skill diff --git a/skills/generating-webapp-graphql-mutation-query/SKILL.md b/skills/generating-webapp-graphql-mutation-query/SKILL.md deleted file mode 100644 index f864a49..0000000 --- a/skills/generating-webapp-graphql-mutation-query/SKILL.md +++ /dev/null @@ -1,258 +0,0 @@ ---- -name: generating-webapp-graphql-mutation-query -description: Generate Salesforce GraphQL mutation queries. Use when the query to generate is a mutation query. Schema exploration must complete first — invoke exploring-graphql-schema first. -paths: - - "**/*.ts" - - "**/*.tsx" - - "**/*.graphql" ---- - -# Salesforce GraphQL Mutation Query Generation - -**Triggering conditions** - -1. Only if the schema exploration phase completed successfully (invoke `exploring-graphql-schema` first) -2. Only if the query to generate is a mutation query - -## Schema Access Policy - -> ⚠️ **GREP ONLY** — During mutation generation you may need to verify field names, input types, or representations. All schema lookups **MUST** use the grep-only commands defined in the `exploring-graphql-schema` skill. Do NOT open, read, stream, or parse `./schema.graphql` with any tool other than grep. - -## Your Role - -You are a GraphQL expert. Generate Salesforce-compatible mutation queries. Schema exploration must complete first. If the schema exploration has not been executed yet, you **MUST** run the full exploration workflow from the `exploring-graphql-schema` skill first, then return here for mutation query generation. - -## Mutation Queries General Information - -The GraphQL engine supports `Create`, `Update`, and `Delete` operations. `Update` and `Delete` operate on Id-based entity identification. See the [mutation query schema](#mutation-query-schema) section. - -## Mutation Query Generation Workflow - -Strictly follow the rules below when generating the GraphQL mutation query: - -1. **Input Fields Validation** - Validate that the set of fields validate [input field constraints](#mutation-queries-input-field-constraints). Verify every field name and type against grep output from the schema — do NOT guess or assume -2. **Output Fields Validation** - Validate that the set of fields used in the select part of the query validate the [output fields constraints](#mutation-queries-output-field-constraints) -3. **Type Consistency** - Make sure variables used as query arguments and their related fields share the same GraphQL type. Verify types via grep lookup — do NOT assume types -4. **Report Phase** - Use the [Mutation Query Report Template](#mutation-query-report-template) below to report on the previous validation phases -5. **Input Arguments** - `input` is the default name for the argument, unless otherwise specified -6. **Output Field** - For `Create` and `Update` operations, the output field is always named `Record`, and is of type EntityName -7. **Field Name Validation** - Every field name in the generated mutation **MUST** match a field confirmed via grep lookup in the schema. Do NOT guess or assume field names exist -8. **Query Generation** - Use the [mutation query](#mutation-query-templates) template and adjust it based on the selected operation -9. **Output Format** - Use the [standalone](#mutation-standalone-default-output-format---clean-code-only) -10. **Lint Validation** - After writing the mutation to a file, run `npx eslint ` from the webapp dir to validate it against the schema. Fix any reported errors before proceeding. See [Lint Validation](#lint-validation) for details -11. **Test the Query** - Use the [Generated Mutation Query Testing](#generated-mutation-query-testing) workflow to test the generated query - 1. **Report First** - Always output the generated mutation in the proper output format BEFORE initiating any test - -## Mutation Query Schema - -**Important**: In the schema fragments below, replace **EntityName** occurrences by the real entity name (i.e. Account, Case...). -**Important**: `Delete` operations all share the same generic `Record` entity name for both input and payload, only exposing the standard `Id` field. - -```graphql -input EntityNameCreateRepresentation { - # Subset of EntityName fields here -} -input EntityNameCreateInput { EntityName: EntityNameCreateRepresentation! } -type EntityNameCreatePayload { Record: EntityName! } - -input EntityNameUpdateRepresentation { - # Subset of EntityName fields here -} -input EntityNameUpdateInput { Id: IdOrRef! EntityName: EntityNameUpdateRepresentation! } -type EntityNameUpdatePayload { Record: EntityName! } - -input RecordDeleteInput { Id: IdOrRef! } -type RecordDeletePayload { Id: ID } - -type UIAPIMutations { - EntityNameCreate(input: EntityNameCreateInput!): EntityNameCreatePayload - EntityNameDelete(input: RecordDeleteInput!): RecordDeletePayload - EntityNameUpdate(input: EntityNameUpdateInput!): EntityNameUpdatePayload -} -``` - -## Mutation Queries Input Field Constraints - -1. **`Create` Mutation Queries**: - 1. **MUST** include all required fields - 2. **MUST** only include createable fields - 3. Child relationships can't be set and **MUST** be excluded - 4. Fields with type `REFERENCE` can only be assigned IDs through their `ApiName` name -2. **`Update` Mutation Queries**: - 1. **MUST** include the id of the entity to update - 2. **MUST** only include updateable fields - 3. Child relationships can't be set and **MUST** be excluded - 4. Fields with type `REFERENCE` can only be assigned IDs through their `ApiName` name -3. **`Delete` Mutation Queries**: - 1. **MUST** include the id of the entity to delete - -## Mutation Queries Output Field Constraints - -1. **`Create` and `Update` Mutation Queries**: - 1. **MUST** exclude all child relationships - 2. **MUST** exclude all `REFERENCE` fields, unless accessed through their `ApiName` member (no navigation to referenced entity) - 3. Inaccessible fields will be reported as part of the `errors` attribute in the returned payload - 4. Child relationships **CAN'T** be queried as part of a mutation - 5. Fields with type `REFERENCE` can only be queried through their `ApiName` (no referenced entities navigation, no sub fields) -2. **`Delete` Mutation Queries**: - 1. **MUST** only include the `Id` field - -## Mutation Query Report Template - -Input arguments: - -- Required fields: FieldName1 (Type1), FieldName2 (Type2)... -- Other fields: FieldName3 (Type3)... - Output fields: FieldNameA (TypeA), FieldNameB (TypeB)... - -## Mutation Query Templates - -```graphql -mutation mutateEntityName( - # arguments -) { - uiapi { - EntityNameOperation(input: { - # the following is for `Create` and `Update` operations only - EntityName: { - # Input fields - } - # the following is for `Update` and `Delete` operations only - Id: ... # id here - }) { - # the following is for `Create` and `Update` operations only - Record { - # Output fields - } - # the following is for `Delete` operations only - Id: ... # id here - } - } -} -``` - -## Mutation Standalone (Default) Output Format - CLEAN CODE ONLY - -```javascript -import { gql } from '@salesforce/sdk-data'; -const QUERY_NAME = gql` - mutation mutateEntity($input: EntityNameOperationInput!) { - uiapi { - EntityNameOperation(input: $input) { - # select output fields here depending on operation type - } - } - } -`; - -const QUERY_VARIABLES = { - input: { - // The following is for `Create` and `Update` operations only - EntityName: { - // variables here - }, - // The following is for `Update` and `Delete` operations only - Id: ... // id here - } -}; -``` - -**❌ FORBIDDEN — Do NOT include any of the following:** - -- Explanatory comments about the query (inline or surrounding) -- Field descriptions or annotations -- Additional text about what the query does -- Workflow step descriptions or summaries -- Comments like `// fetches...`, `// creates...`, `/* ... */` - -**✅ ONLY output:** - -- The raw query string constant (using `gql` tagged template) -- The variables object constant -- Nothing else — no extra imports, no exports, no wrapper functions - -## Lint Validation - -After writing the generated mutation into a source file, validate it against the schema using the project's GraphQL ESLint setup: - -```bash -# Run from webapp dir (force-app/main/default/webapplications//) -npx eslint -``` - -**How it works:** The ESLint config uses `@graphql-eslint/eslint-plugin` with its `processor`, which extracts GraphQL operations from `gql` template literals in `.ts`/`.tsx` files and validates the extracted `.graphql` virtual files against `schema.graphql`. - -**Rules enforced:** `no-anonymous-operations`, `no-duplicate-fields`, `known-fragment-names`, `no-undefined-variables`, `no-unused-variables` - -**On failure:** Fix the reported issues, re-run `npx eslint ` until clean, then proceed to testing. - -> ⚠️ **Prerequisites**: The `schema.graphql` file must exist (invoke `exploring-graphql-schema` first) and project dependencies must be installed (`npm install`). - -## Generated Mutation Query Testing - -**Triggering conditions** — **ALL conditions must be true:** - -1. The [Mutation Query Generation Workflow](#mutation-query-generation-workflow) completed with status `SUCCESS` and you have a generated query -2. The query is a mutation query -3. A non-manual method was used during schema exploration to retrieve introspection data - -**Workflow** - -1. **Report Step** - State the exact method you will use to test (e.g., `sf api request graphql` from the **project root**, Connect API, etc.) — this **MUST** match the method used during schema exploration -2. **Interactive Step** - Ask the user whether they want you to test the query using the proposed method - 1. **STOP and WAIT** for the user's answer. Do NOT proceed until the user responds. Do NOT assume consent. -3. **Input Arguments** - You **MUST** ask the user for the input argument values to use in the test - 1. **STOP and WAIT** for the user's answer. Do NOT proceed until the user provides values. Do NOT fabricate test data. -4. **Test Query** - Only if the user explicitly agrees and has provided input values: - 1. Execute the mutation using the reported method (e.g., `sf api request rest` to POST the query and variables to the GraphQL endpoint): - ```bash - sf api request rest /services/data/v65.0/graphql \ - --method POST \ - --body '{"query":"mutation mutateEntity($input: EntityNameOperationInput!) { uiapi { EntityNameOperation(input: $input) { Record { Id } } } }","variables":{"input":{"EntityName":{"Field":"Value"}}}}' - ``` - 2. Replace `v65.0` with the API version of the target org - 3. Replace the `query` value with the generated mutation query string - 4. Replace the `variables` value with the user-provided input arguments -5. **Result Analysis** - Retrieve the `data` and `errors` attributes from the returned payload, and report the result of the test as one of the following options: - 1. `PARTIAL` if `data` is not an empty object, but `errors` is not an empty list - Explanation: some of the queried fields are not accessible on mutations - 2. `FAILED` if `data` is an empty object - Explanation: the query is not valid - 3. `SUCCESS` if `errors` is an empty list -6. **Remediation Step** - If status is not `SUCCESS`, use the [`FAILED`](#failed-status-handling-workflow) or [`PARTIAL`](#partial-status-handling-workflow) status handling workflows - -### `FAILED` Status Handling Workflow - -The query is invalid: - -1. **Error Analysis** - Parse and categorize the specific error messages -2. **Root Cause Identification** - Use error message to identify the root cause: - - **Execution** - Error contains `invalid cross reference id` or `entity is deleted` - - **Syntax** - Error contains `invalid syntax` - - **Validation** - Error contains `validation error` - - **Type** - Error contains `VariableTypeMismatch` or `UnknownType` - - **Navigation** - Error contains `is not currently available in mutation results` - - **API Version** - Query deals with updates, you're testing with Connect API and error contains `Cannot invoke JsonElement.isJsonObject()` -3. **Targeted Resolution** - Depending on the root cause categorization - - **Execution** - You're trying to update or delete an unknown/no longer available entity: either create an entity first, if you have generated the related query, or ask for a valid entity id to use. **STOP and WAIT** for the user to provide a valid Id - - **Syntax** - Update the query using the error message information to fix the syntax errors - - **Validation** - The field name is most probably invalid. Re-run the relevant grep command from the `exploring-graphql-schema` skill to verify the correct field name. If still unclear, ask the user for clarification and **STOP and WAIT** for their answer - - **Type** - Use the error details and re-verify the type via grep lookup in the schema. Correct the argument type and adjust variables accordingly - - **Navigation** - Use the [`PARTIAL` status handling workflow](#partial-status-handling-workflow) below - - **API Version** - `Record` selection is only available with API version 64 and higher, **report** the issue, and try again with API version 64 -4. **Test Again** - Resume the [query testing workflow](#generated-mutation-query-testing) with the updated query (increment and track attempt counter) -5. **Escalation Path** - If targeted resolution fails after 2 attempts, ask for additional details and restart the entire GraphQL workflow from the `exploring-graphql-schema` skill - -### `PARTIAL` Status Handling Workflow - -The query can be improved: - -1. Report the fields mentioned in the `errors` list -2. Explain that these fields can't be queried as part of a mutation query -3. Explain that the query might be considered as failing, as it will report errors -4. Offer to remove the offending fields -5. **STOP and WAIT** for the user's answer. Do NOT remove fields without explicit consent. -6. If they are OK with removing the fields restart the [generation workflow](#mutation-query-generation-workflow) with the new field list - -## Related Skills - -- Schema exploration: `exploring-graphql-schema` (must complete first) -- Read query generation: `generating-graphql-read-query` diff --git a/skills/generating-webapp-graphql-read-query/SKILL.md b/skills/generating-webapp-graphql-read-query/SKILL.md deleted file mode 100644 index 051ec8f..0000000 --- a/skills/generating-webapp-graphql-read-query/SKILL.md +++ /dev/null @@ -1,253 +0,0 @@ ---- -name: generating-webapp-graphql-read-query -description: Generate Salesforce GraphQL read queries. Use when the query to generate is a read query. Schema exploration must complete first — invoke exploring-graphql-schema first. -paths: - - "**/*.ts" - - "**/*.tsx" - - "**/*.graphql" ---- - -# Salesforce GraphQL Read Query Generation - -**Triggering conditions** - -1. Only if the schema exploration phase completed successfully (invoke `exploring-graphql-schema` first) -2. Only if the query to generate is a read query - -## Schema Access Policy - -> ⚠️ **GREP ONLY** — During query generation you may need to verify field names, types, or relationships. All schema lookups **MUST** use the grep-only commands defined in the `exploring-graphql-schema` skill. Do NOT open, read, stream, or parse `./schema.graphql` with any tool other than grep. - -## Field-Level Security and @optional - -Field-level security (FLS) restricts which fields different users can see. Use the `@optional` directive on Salesforce record fields when possible. The server omits the field when the user lacks access, allowing the query to succeed instead of failing. Apply `@optional` to scalar fields, value-type fields (e.g. `Name { value }`), parent relationships, and child relationships. Available in API v65.0+. - -**Consuming code must defend against missing fields.** When a field is omitted due to FLS, it will be `undefined` (or absent) in the response. Use optional chaining (`?.`), nullish coalescing (`??`), and explicit null/undefined checks when reading query results. Never assume an optional field is present — otherwise the app may crash or behave incorrectly for users without field access. - -```ts -// ✅ Defend against missing fields -const name = node.Name?.value ?? ''; -const relatedName = node.RelationshipName?.Name?.value ?? 'N/A'; - -// ❌ Unsafe — will throw if field omitted due to FLS -const name = node.Name.value; -``` - -## Your Role - -You are a GraphQL expert. Generate Salesforce-compatible read queries. Schema exploration must complete first. If the schema exploration has not been executed yet, you **MUST** run the full exploration workflow from the `exploring-graphql-schema` skill first, then return here for read query generation. - -## Read Query Generation Workflow - -Strictly follow the rules below when generating the GraphQL read query: - -1. **No Proliferation** - Only generate for the explicitly requested fields, nothing else. Do NOT add fields the user did not ask for. -2. **Unique Query** - Leverage child relationships to query entities in one single query -3. **Navigate Entities** - Always use `relationshipName` to access reference fields and child entities - 1. **Exception** - if the `relationshipName` field is null, you can't navigate the related entity, and will have to return the `Id` itself -4. **Leverage Fragments** - Generate one fragment per possible type on polymorphic fields (field with `dataType="REFERENCE"` and more than one entry in `referenceToInfos` introspection attribute) -5. **Type Consistency** - Make sure variables used as query arguments and their related fields share the same GraphQL type. Verify types against grep output from the schema — do not assume types -6. **Type Enforcement** - Make sure to leverage field type information from introspection and GraphQL schema to generate field access -7. **Field Name Validation** - Every field name in the generated query **MUST** match a field confirmed via grep lookup in the schema. Do NOT guess or assume field names exist -8. **@optional for FLS** - Apply `@optional` on all Salesforce record fields when possible (see [Field-Level Security and @optional](#field-level-security-and-optional)). This lets the query succeed when the user lacks field-level access; the server omits inaccessible fields instead of failing -9. **Consuming code defense** - When generating or modifying code that consumes read query results, defend against missing fields (see [Field-Level Security and @optional](#field-level-security-and-optional)). Use optional chaining, nullish coalescing, and null/undefined checks — never assume optional fields are present -10. **Semi and anti joins** - Use the semi-join or anti-join templates to filter an entity with conditions on child entities -11. **Query Generation** - Use the [template](#read-query-template) to generate the query -12. **Output Format** - Use the [standalone](#read-standalone-default-output-format---clean-code-only) -13. **Lint Validation** - After writing the query to a file, run `npx eslint ` from the webapp dir to validate it against the schema. Fix any reported errors before proceeding. See [Lint Validation](#lint-validation) for details -14. **Test the Query** - Use the [Generated Read Query Testing](#generated-read-query-testing) workflow to test the generated query - 1. **Report First** - Always output the generated query in the proper output format BEFORE initiating any test - -## Read Query Template - -```graphql -query QueryName { - uiapi { - query { - EntityName( - # conditions here - ) { - edges { - node { - # Direct fields — use @optional for FLS resilience - FieldName @optional { value } - - # Non-polymorphic reference (single type) - RelationshipName @optional { - Id - Name { value } - } - - # Polymorphic reference (multiple types) - PolymorphicRelationshipName @optional { - ...TypeAInfo - ...TypeBInfo - } - - # Child relationship (subquery) - RelationshipName @optional ( - # conditions here - ) { - edges { - node { - # fields - } - } - } - } - } - } - } - } -} - -fragment TypeAInfo on TypeA { - Id - SpecificFieldA @optional { value } -} - -fragment TypeBInfo on TypeB { - Id - SpecificFieldB @optional { value } -} -``` - -## Semi-Join and Anti-Join Condition Template - -Semi-joins (resp. anti-joins) condition leverage parent-child relationships and allow filtering the parent entity using a condition on child entities. -This is a standard `where` condition, on the parent entity's `Id`, expressed using the `inq` (resp. `ninq`, i.e. not `inq`) operator. This operator accepts two attributes: - -- The child entity camelcase name to apply the condition on, with a value expressing the condition -- The field name on the child entity containing the parent entity `Id`, which is the `fieldName` from the `childRelationships` information for the child entity -- If the only condition is related child entity existence, you can use an `Id: { ne: null }` condition - -### Semi-Join Example - ParentEntity with at least one Matching ChildEntity - -```graphql -query testSemiJoin { - uiapi { - query { - ParentEntity( - where: { - Id: { - inq: { - ChildEntity: { - # standard conditions here - Name: { like: "test%" } - Type: { eq: "some value" } - } - ApiName: "parentIdFieldInChild" - } - } - } - ) { - edges { - node { - Id - Name @optional { - value - } - } - } - } - } - } -} -``` - -### Anti-Join Example - ParentEntity with no Matching ChildEntity - -Same example as the [Semi-Join Example](#semi-join-example---parententity-with-at-least-one-matching-childentity), but replacing the `inq` operator by the `ninq` one. - -## Read Standalone (Default) Output Format - CLEAN CODE ONLY - -```javascript -const QUERY_NAME = ` - query GetData { - # query here - } -`; - -const QUERY_VARIABLES = { - // variables here -}; -``` - -**❌ FORBIDDEN — Do NOT include any of the following:** - -- Explanatory comments about the query (inline or surrounding) -- Field descriptions or annotations -- Additional text about what the query does -- Workflow step descriptions or summaries -- Comments like `// fetches...`, `// returns...`, `/* ... */` - -**✅ ONLY output:** - -- The raw query string constant -- The variables object constant -- Nothing else — no imports, no exports, no wrapper functions - -## Lint Validation - -After writing the generated query into a source file, validate it against the schema using the project's GraphQL ESLint setup: - -```bash -# Run from webapp dir (force-app/main/default/webapplications//) -npx eslint -``` - -**How it works:** The ESLint config uses `@graphql-eslint/eslint-plugin` with its `processor`, which extracts GraphQL operations from `gql` template literals in `.ts`/`.tsx` files and validates the extracted `.graphql` virtual files against `schema.graphql`. - -**Rules enforced:** `no-anonymous-operations`, `no-duplicate-fields`, `known-fragment-names`, `no-undefined-variables`, `no-unused-variables` - -**On failure:** Fix the reported issues, re-run `npx eslint ` until clean, then proceed to testing. - -> ⚠️ **Prerequisites**: The `schema.graphql` file must exist (invoke `exploring-graphql-schema` first) and project dependencies must be installed (`npm install`). - -## Generated Read Query Testing - -**Triggering conditions** — **ALL conditions must be true:** - -1. The [Read Query Generation Workflow](#read-query-generation-workflow) completed with status `SUCCESS` and you have a generated query -2. The query is a read query -3. A non-manual method was used during schema exploration to retrieve introspection data - -**Workflow** - -1. **Report Step** - State the exact method you will use to test (e.g., `sf api request graphql` from the **project root**, Connect API, etc.) — this **MUST** match the method used during schema exploration -2. **Interactive Step** - Ask the user whether they want you to test the query using the proposed method - 1. **STOP and WAIT** for the user's answer. Do NOT proceed until the user responds. Do NOT assume consent. -3. **Test Query** - Only if the user explicitly agrees: - 1. Use `sf api request rest` to POST the query to the GraphQL endpoint: - ```bash - sf api request rest /services/data/v65.0/graphql \ - --method POST \ - --body '{"query":"query GetData { uiapi { query { EntityName { edges { node { Id } } } } } }"}' - ``` - 2. Replace `v65.0` with the API version of the target org - 3. Replace the `query` value with the generated read query string - 4. If the query uses variables, include them in the JSON body as a `variables` key - 5. Report the result as `SUCCESS` if the query executed without error, or `FAILED` if errors were returned - 6. An empty result set with no errors is `SUCCESS` — the query is valid, the org simply has no matching data -4. **Remediation Step** - If status is `FAILED`, use the [`FAILED` status handling workflows](#failed-status-handling-workflow) - -### `FAILED` Status Handling Workflow - -The query is invalid: - -1. **Error Analysis** - Parse and categorize the specific error messages -2. **Root Cause Identification** - Use error message to identify the root cause: - - **Syntax** - Error contains `invalid syntax` - - **Validation** - Error contains `validation error` - - **Type** - Error contains `VariableTypeMismatch` or `UnknownType` -3. **Targeted Resolution** - Depending on the root cause categorization - - **Syntax** - Update the query using the error message information to fix the syntax errors - - **Validation** - The field name is most probably invalid. Re-run the relevant grep command from the `exploring-graphql-schema` skill to verify the correct field name. If still unclear, ask the user for clarification and **STOP and WAIT** for their answer - - **Type** - Use the error details and re-verify the type via grep lookup in the schema. Correct the argument type and adjust variables accordingly -4. **Test Again** - Resume the [query testing workflow](#generated-read-query-testing) with the updated query (increment and track attempt counter) -5. **Escalation Path** - If targeted resolution fails after 2 attempts, ask for additional details and restart the entire GraphQL workflow from the `exploring-graphql-schema` skill - -## Related Skills - -- Schema exploration: `exploring-graphql-schema` (must complete first) -- Mutation generation: `generating-graphql-mutation-query` diff --git a/skills/implementing-webapp-file-upload/SKILL.md b/skills/implementing-webapp-file-upload/SKILL.md index d365a8c..ef1ba16 100644 --- a/skills/implementing-webapp-file-upload/SKILL.md +++ b/skills/implementing-webapp-file-upload/SKILL.md @@ -1,6 +1,6 @@ --- name: implementing-webapp-file-upload -description: Add file upload functionality to React webapps with progress tracking and Salesforce ContentVersion integration. Use when the user wants to upload files, attach documents, handle file input, create file dropzones, track upload progress, or link files to Salesforce records. This feature provides programmatic APIs ONLY — no components or hooks are exported. Build your own custom UI using the upload() API. ALWAYS use this feature instead of building file upload from scratch with FormData or XHR. +description: "Add file upload functionality to React webapps with progress tracking and Salesforce ContentVersion integration. Use when the user wants to upload files, attach documents, handle file input, create file dropzones, track upload progress, or link files to Salesforce records. This feature provides programmatic APIs ONLY — no components or hooks are exported. Build your own custom UI using the upload() API. ALWAYS use this feature instead of building file upload from scratch with FormData or XHR." --- # File Upload API (workflow) diff --git a/skills/installing-webapp-features/SKILL.md b/skills/installing-webapp-features/SKILL.md index 80bf7af..35aafb1 100644 --- a/skills/installing-webapp-features/SKILL.md +++ b/skills/installing-webapp-features/SKILL.md @@ -1,6 +1,6 @@ --- name: installing-webapp-features -description: Search, describe, and install pre-built UI features (authentication, shadcn components, navigation, search, GraphQL, Agentforce AI) into Salesforce webapps. Use this when the user wants to add functionality to a webapp, or when determining what salesforce-provided features are available — whether prompted by the user or on your own initiative. Always check for an existing feature before building from scratch. +description: "Search, describe, and install pre-built UI features (authentication, shadcn components, navigation, search, GraphQL, Agentforce AI) into Salesforce webapps. Use this when the user wants to add functionality to a webapp, or when determining what salesforce-provided features are available — whether prompted by the user or on your own initiative. Always check for an existing feature before building from scratch." --- # webapps-features-experimental CLI — Agent Reference diff --git a/skills/managing-webapp-agentforce-conversation-client/SKILL.md b/skills/managing-webapp-agentforce-conversation-client/SKILL.md index 846805d..0fd7aa3 100644 --- a/skills/managing-webapp-agentforce-conversation-client/SKILL.md +++ b/skills/managing-webapp-agentforce-conversation-client/SKILL.md @@ -1,6 +1,6 @@ --- name: managing-webapp-agentforce-conversation-client -description: Adds or modifies AgentforceConversationClient in React apps (.tsx or .jsx files). Use when user says "add chat widget", "embed agentforce", "add agent", "add chatbot", "integrate conversational AI", or asks to change colors, dimensions, styling, or configure agentId, width, height, inline mode, or styleTokens for travel agent, HR agent, employee agent, or any Salesforce agent chat. +description: "Adds or modifies AgentforceConversationClient in React apps (.tsx or .jsx files). Use when user says \"add chat widget\", \"embed agentforce\", \"add agent\", \"add chatbot\", \"integrate conversational AI\", or asks to change colors, dimensions, styling, or configure agentId, width, height, inline mode, or styleTokens for travel agent, HR agent, employee agent, or any Salesforce agent chat." metadata: author: ACC Components version: 1.0.0 diff --git a/skills/trigger-refactor-pipeline/SKILL.md b/skills/trigger-refactor-pipeline/SKILL.md index 9236c2c..dbd6185 100644 --- a/skills/trigger-refactor-pipeline/SKILL.md +++ b/skills/trigger-refactor-pipeline/SKILL.md @@ -1,6 +1,6 @@ --- name: trigger-refactor-pipeline -description: Refactor Salesforce triggers into handler patterns with automated test generation and deployment. Use when modernizing legacy triggers with DML/SOQL in loops or inconsistent patterns. +description: "Refactor Salesforce triggers into handler patterns with automated test generation and deployment. Use when modernizing legacy triggers with DML/SOQL in loops or inconsistent patterns." license: Apache-2.0 compatibility: Requires Salesforce CLI, Python 3.9+ metadata: diff --git a/skills/using-webapp-graphql/SKILL.md b/skills/using-webapp-graphql/SKILL.md deleted file mode 100644 index 838f0c3..0000000 --- a/skills/using-webapp-graphql/SKILL.md +++ /dev/null @@ -1,324 +0,0 @@ ---- -name: using-webapp-graphql -description: Salesforce GraphQL data access. Use when the user asks to fetch, query, or mutate Salesforce data, or add a GraphQL operation for an object like Account, Contact, or Opportunity. -paths: - - "**/*.ts" - - "**/*.tsx" - - "**/*.graphql" ---- - -# Salesforce GraphQL - -Guidance for querying and mutating Salesforce data via the Salesforce GraphQL API. Use `createDataSDK()` + `sdk.graphql?.()` and codegen tooling. - -## When to Use - -- User asks to "fetch data from Salesforce" -- User asks to "query" or "mutate" Salesforce records -- User wants to add a new GraphQL operation (query or mutation) -- User asks to add data access for a Salesforce object (Account, Contact, Opportunity, etc.) - -## Schema Access Policy (GREP ONLY) - -> **GREP ONLY** — The `schema.graphql` file is very large (~265,000+ lines). All schema lookups **MUST** use the grep-only commands defined in the `exploring-graphql-schema` skill. Do NOT open, read, stream, or parse `./schema.graphql` with any tool other than grep. - -## Directory Context - -The generated app has a two-level directory structure. Commands must run from the correct directory. - -``` -/ ← SFDX project root -├── schema.graphql ← grep target -├── sfdx-project.json -└── force-app/main/default/webapplications// ← webapp dir - ├── package.json (npm scripts: graphql:schema, graphql:codegen, lint) - ├── eslint.config.js (schema ref: ../../../../../schema.graphql) - ├── codegen.yml (schema ref: ../../../../../schema.graphql) - └── src/ (source code, .graphql query files) -``` - -| Command | Run from | Why | -| ------------------------- | ---------------- | -------------------------------------- | -| `npm run graphql:schema` | **webapp dir** | Script is in webapp's `package.json` | -| `npm run graphql:codegen` | **webapp dir** | Reads `codegen.yml` in webapp dir | -| `npx eslint ` | **webapp dir** | Reads `eslint.config.js` in webapp dir | -| `grep ... schema.graphql` | **project root** | `schema.graphql` lives at project root | -| `sf api request graphql` | **project root** | Needs `sfdx-project.json` | - -> **Wrong directory = silent failures.** `npm run graphql:schema` from the project root will fail with "missing script." `grep ./schema.graphql` from the webapp dir will fail with "no such file." - -## Prerequisites - -The base React app (`base-react-app`) ships with all GraphQL dependencies and tooling pre-configured: - -- `@salesforce/sdk-data` — runtime SDK for `createDataSDK` and `gql` -- `@graphql-codegen/cli` + plugins — type generation from `.graphql` files and inline `gql` queries -- `@graphql-eslint/eslint-plugin` — validates `.graphql` files and `gql` template literals against `schema.graphql` (used as a query validation gate — see Step 6) -- `graphql` — shared by codegen, ESLint, and schema introspection - -Before using this skill, ensure: - -1. The `@salesforce/sdk-data` package is available (provides `createDataSDK`, `gql`, `NodeOfConnection`) -2. **Deployment order**: Metadata must be deployed before schema fetch; schema must be refetched after any metadata deployment. Invoke the `deploying-to-salesforce` skill when deploying or syncing with the org. -3. A `schema.graphql` file exists at the project root. If missing, generate it: - ```bash - # Run from webapp dir (force-app/main/default/webapplications//) - npm run graphql:schema - ``` - -## npm Scripts - -- **`npm run graphql:schema`** — _(run from webapp dir)_ Downloads the full GraphQL schema from a connected Salesforce org via introspection. Outputs `schema.graphql` to the project root. -- **`npm run graphql:codegen`** — _(run from webapp dir)_ Generates TypeScript types from `.graphql` files and inline `gql` queries. Outputs to `src/api/graphql-operations-types.ts`. - -## Workflow - -### Step 1: Download Schema - -Ensure `schema.graphql` exists at the project root. If missing, run `npm run graphql:schema` from the webapp dir. - -### Step 2: Explore the Schema (grep-only) - -Before writing any query, verify the target object and its fields exist in the schema. - -**Invoke the `exploring-graphql-schema` skill** for the full exploration workflow and **mandatory grep-only access policy**. - -> **GREP ONLY** — All schema lookups MUST use the grep commands defined in the `exploring-graphql-schema` skill. Do NOT open, read, stream, or parse `./schema.graphql` with any tool other than grep. - -Key actions (all via grep): - -- `type implements Record` — find available fields -- `input _Filter` — find filter options -- `input _OrderBy` — find sorting options -- `input CreateInput` / `UpdateInput` — find mutation input types - -### Step 3: Choose the Query Pattern - -**Pattern 1 — External `.graphql` file** (recommended for complex queries): - -- Queries with variables, fragments, or shared across files -- Full codegen support, syntax highlighting, shareable -- Requires codegen step after changes -- See example: `api/utils/accounts.ts` + `api/utils/query/highRevenueAccountsQuery.graphql` - -**Pattern 2 — Inline `gql` tag** (recommended for simple queries): - -- Simple queries without variables; colocated with usage code -- Supports dynamic queries (field set varies at runtime) -- **MUST use `gql` tag** — plain template strings bypass `@graphql-eslint` validation -- See example: `api/utils/user.ts` - -### Step 4: Write the Query - -For **Pattern 1**: - -1. Create a `.graphql` file under `src/api/utils/query/` -2. Follow UIAPI structure: `query { uiapi { query { ObjectName(...) { edges { node { ... } } } } } }` -3. For mutations, invoke the `generating-graphql-mutation-query` skill -4. For read queries, invoke the `generating-graphql-read-query` skill - -For **Pattern 2**: - -1. Define query inline using the `gql` template tag -2. Ensure the query name matches what codegen expects - -### Step 5: Test Queries Against Live Org - -Use the testing workflows in the `generating-graphql-read-query` and `generating-graphql-mutation-query` skills to validate queries against the connected org before integrating into the app. - -### Step 6: Generate Types - -```bash -# Run from webapp dir (force-app/main/default/webapplications//) -npm run graphql:codegen -``` - -This updates `src/api/graphql-operations-types.ts` with `Query`/`Mutation` and `QueryVariables`/`MutationVariables`. - -### Step 7: Lint Validate - -Run ESLint on the file containing the query to validate it against the schema **before** any live testing: - -```bash -# Run from webapp dir -npx eslint -``` - -The `@graphql-eslint/eslint-plugin` processor extracts GraphQL from `gql` template literals and validates them against `schema.graphql`. Fix all ESLint errors before proceeding. - -### Step 8: Implement and Verify - -Implement the data access function using the pattern below. Use the Quality Checklist before completing. - ---- - -## Core Types & Function Signatures - -### createDataSDK and graphql - -```typescript -import { createDataSDK } from "@salesforce/sdk-data"; - -const sdk = await createDataSDK(); -const response = await sdk.graphql?.(query, variables); -``` - -`createDataSDK()` returns a `DataSDK` instance. The `graphql` method uses optional chaining (`?.`) because not all surfaces support GraphQL. - -### gql Template Tag - -```typescript -import { gql } from "@salesforce/sdk-data"; - -const MY_QUERY = gql` - query MyQuery { - uiapi { ... } - } -`; -``` - -The `gql` tag enables ESLint validation against the schema. Plain template strings bypass validation. - -### Error Handling - -Default: treat any errors as failure (Strategy A). For partial data tolerance, log errors but use data. For mutations where some return fields are inaccessible, use Strategy C (fail only when no data). - -```typescript -// Default: strict -if (response?.errors?.length) { - throw new Error(response.errors.map((e) => e.message).join("; ")); -} -const result = response?.data; -``` - -Responses follow `uiapi.query.ObjectName.edges[].node`; fields use `{ value }`. - -### NodeOfConnection - -```typescript -import { type NodeOfConnection } from "@salesforce/sdk-data"; - -type AccountNode = NodeOfConnection; -``` - ---- - -## Pattern 1: External .graphql File - -Create a `.graphql` file, run `npm run graphql:codegen`, import with `?raw` suffix, and use generated types. - -**Required imports:** - -```typescript -import { createDataSDK, type NodeOfConnection } from "@salesforce/sdk-data"; -import MY_QUERY from "./query/myQuery.graphql?raw"; // ← ?raw suffix required -import type { GetMyDataQuery, GetMyDataQueryVariables } from "../graphql-operations-types"; -``` - -**When to use:** Complex queries with variables, fragments, or shared across files. Does NOT support dynamic queries (field set varies at runtime). - ---- - -## Pattern 2: Inline gql Tag - -**Required imports:** - -```typescript -import { createDataSDK, gql } from "@salesforce/sdk-data"; -import { type CurrentUserQuery } from "../graphql-operations-types"; - -const MY_QUERY = gql` - query CurrentUser { - uiapi { ... } - } -`; -``` - -> **MUST use `gql` tag** — plain template strings bypass the `@graphql-eslint` processor entirely, meaning no lint validation against the schema. - -**When to use:** Simple, colocated queries. Supports dynamic queries (field set varies at runtime). - ---- - -## Conditional Field Selection - -For dynamic fieldsets with **known** fields, use `@include(if: $condition)` and `@skip(if: $condition)` in `.graphql` files. See GraphQL spec for details. - ---- - -## Anti-Patterns (Not Recommended) - -### Direct API Calls - -```typescript -// NOT RECOMMENDED: Direct axios/fetch calls for GraphQL -// PREFERRED: Use the Data SDK -const sdk = await createDataSDK(); -const response = await sdk.graphql?.(query, variables); -``` - -### Missing Type Definitions - -```typescript -// NOT RECOMMENDED: Untyped GraphQL calls -// PREFERRED: Provide response type -const response = await sdk.graphql?.(query); -``` - -### Plain String Queries (Without gql Tag) - -```typescript -// NOT RECOMMENDED: Plain strings bypass ESLint validation -const query = `query { ... }`; - -// PREFERRED: Use gql tag for inline queries -const QUERY = gql`query { ... }`; -``` - ---- - -## Quality Checklist - -> If you have not completed the workflow above, **stop and complete it first**. Invoke the skill workflow before using this checklist. - -Before completing GraphQL data access code: - -### For Pattern 1 (.graphql files): - -1. [ ] All field names verified via grep against `schema.graphql` (invoke `exploring-graphql-schema`) -2. [ ] Create `.graphql` file for the query/mutation -3. [ ] Run `npm run graphql:codegen` to generate types -4. [ ] Import query with `?raw` suffix -5. [ ] Import generated types from `graphql-operations-types.ts` -6. [ ] Use `sdk.graphql?.()` with proper generic -7. [ ] Handle `response.errors` and destructure `response.data` -8. [ ] Use `NodeOfConnection` for cleaner node types when needed -9. [ ] Run `npx eslint ` from webapp dir — fix all GraphQL errors - -### For Pattern 2 (inline with gql): - -1. [ ] All field names verified via grep against `schema.graphql` -2. [ ] Define query using `gql` template tag (NOT a plain string) -3. [ ] Ensure query name matches generated types in `graphql-operations-types.ts` -4. [ ] Import generated types for the query -5. [ ] Use `sdk.graphql?.()` with proper generic -6. [ ] Handle `response.errors` and destructure `response.data` -7. [ ] Run `npx eslint ` from webapp dir — fix all GraphQL errors - -### General: - -- [ ] Lint validation passes (`npx eslint ` reports no GraphQL errors) -- [ ] Query field names match the schema exactly (case-sensitive, confirmed via grep) -- [ ] Response type generic is provided to `sdk.graphql?.()` -- [ ] Optional chaining is used for nested response data - ---- - -## Reference - -- Schema exploration: invoke the `exploring-graphql-schema` skill -- Read query generation: invoke the `generating-graphql-read-query` skill -- Mutation query generation: invoke the `generating-graphql-mutation-query` skill -- Shared GraphQL schema types: `shared-schema.graphqls` (in this skill directory) -- Schema download: `npm run graphql:schema` (run from webapp dir) -- Type generation: `npm run graphql:codegen` (run from webapp dir) diff --git a/skills/using-webapp-graphql/shared-schema.graphqls b/skills/using-webapp-graphql/shared-schema.graphqls deleted file mode 100644 index e35dc29..0000000 --- a/skills/using-webapp-graphql/shared-schema.graphqls +++ /dev/null @@ -1,1150 +0,0 @@ -# Common scalar types -scalar BigDecimal -scalar Byte -scalar Char -scalar BigInteger -scalar Short -scalar String -scalar Currency -scalar Longitude -scalar Float -scalar PhoneNumber -scalar Email -scalar TextArea -scalar Latitude -scalar RichTextArea -scalar EncryptedString -scalar Long -scalar JSON -scalar Time -scalar Percent -scalar LongTextArea -scalar DateTime -scalar ID -scalar Boolean -scalar MultiPicklist -scalar Base64 -scalar Url -scalar Picklist -scalar Double -scalar Int -scalar IdOrRef -scalar Date - -# Common enums -enum GroupByFunction { - DAY_IN_WEEK - DAY_IN_MONTH - DAY_IN_YEAR - WEEK_IN_MONTH - WEEK_IN_YEAR - CALENDAR_MONTH - CALENDAR_QUARTER - CALENDAR_YEAR - FISCAL_MONTH - FISCAL_QUARTER - FISCAL_YEAR - DAY_ONLY - HOUR_IN_DAY -} - -enum DataType { - STRING - TEXTAREA - PHONE - EMAIL - URL - ENCRYPTEDSTRING - BOOLEAN - CURRENCY - INT - LONG - DOUBLE - PERCENT - DATETIME - TIME - DATE - REFERENCE - PICKLIST - MULTIPICKLIST - ADDRESS - LOCATION - BASE64 - COMPLEXVALUE - COMBOBOX - JSON - JUNCTIONIDLIST - ANYTYPE -} - -enum AggregateOrderByNumberFunction { - AVG - COUNT - COUNT_DISTINCT - MAX - MIN - SUM -} - -enum AggregateOrderByStringFunction { - COUNT - COUNT_DISTINCT - MAX - MIN -} - -enum GroupByType { - GROUP_BY - ROLLUP - CUBE -} - -enum ResultsOrder { - ASC - DESC -} - -enum ResultOrder { - ASC - DESC -} - -enum NullOrder { - FIRST - LAST -} - -enum NullsOrder { - FIRST - LAST -} - -enum DateLiteral { - YESTERDAY - TODAY - TOMORROW - LAST_WEEK - THIS_WEEK - NEXT_WEEK - LAST_MONTH - THIS_MONTH - NEXT_MONTH - LAST_90_DAYS - NEXT_90_DAYS - LAST_QUARTER - THIS_QUARTER - NEXT_QUARTER - LAST_FISCAL_QUARTER - THIS_FISCAL_QUARTER - NEXT_FISCAL_QUARTER - LAST_YEAR - THIS_YEAR - NEXT_YEAR - LAST_FISCAL_YEAR - THIS_FISCAL_YEAR - NEXT_FISCAL_YEAR -} - -enum Unit { - MI - KM -} - -enum SObject__FieldType { - ALL - STANDARD - CUSTOM -} - -enum FieldExtraTypeInfo { - IMAGE_URL - EXTERNAL_LOOKUP - INDIRECT_LOOKUP - PERSONNAME - SWITCHABLE_PERSONNAME - PLAINTEXTAREA - RICHTEXTAREA -} - -enum RecordScope @generic { -} - -# Common interfaces -interface FieldValue { - displayValue: String -} - -interface Field { - ApiName: String! - calculated: Boolean! - compound: Boolean! - compoundComponentName: String - compoundFieldName: String - controllerName: String - controllingFields: [String]! - createable: Boolean! - custom: Boolean! - dataType: DataType - extraTypeInfo: FieldExtraTypeInfo - filterable: Boolean! - filteredLookupInfo: FilteredLookupInfo - highScaleNumber: Boolean! - htmlFormatted: Boolean! - inlineHelpText: String - label: String - nameField: Boolean! - polymorphicForeignKey: Boolean! - precision: Int - reference: Boolean! - referenceTargetField: String - referenceToInfos: [ReferenceToInfo]! - relationshipName: String - required: Boolean! - scale: Int - searchPrefilterable: Boolean - sortable: Boolean! - updateable: Boolean! -} - -interface Record { - Id: ID! - ApiName: String! - WeakEtag: Long! - DisplayValue: String - LastModifiedById: IDValue - LastModifiedDate: DateTimeValue - SystemModstamp: DateTimeValue - RecordTypeId(fallback: Boolean): IDValue -} - -# Common types -type PageInfo { - hasNextPage: Boolean! - hasPreviousPage: Boolean! - startCursor: String - endCursor: String -} - -type SObject__Field { - name: String! - value: String -} - -type ReferenceToInfo { - ApiName: String! - nameFields: [String]! - objectInfo: ObjectInfo -} - -type FilteredLookupInfo { - controllingFields: [String]! - dependent: Boolean! - optionalFilter: Boolean! -} - -type DependentField { - controllingField: String! - dependentFields: [String]! -} - -type RecordTypeInfo { - available: Boolean! - defaultRecordTypeMapping: Boolean! - master: Boolean! - name: String - recordTypeId: ID -} - -type ThemeInfo { - color: String - iconUrl: String -} - -type ChildRelationship { - childObjectApiName: String! - fieldName: String - junctionIdListNames: [String]! - junctionReferenceTo: [String]! - relationshipName: String - objectInfo: ObjectInfo -} - -type ObjectInfo { - ApiName: String! - childRelationships: [ChildRelationship]! - createable: Boolean! - custom: Boolean! - defaultRecordTypeId: ID - deletable: Boolean! - dependentFields: [DependentField]! - feedEnabled: Boolean! - fields: [Field]! - keyPrefix: String - label: String - labelPlural: String - layoutable: Boolean! - mruEnabled: Boolean! - nameFields: [String]! - queryable: Boolean! - recordTypeInfos: [RecordTypeInfo]! - searchable: Boolean! - themeInfo: ThemeInfo - updateable: Boolean! - locale: String -} - -type StandardField implements Field { - ApiName: String! - calculated: Boolean! - compound: Boolean! - compoundComponentName: String - compoundFieldName: String - controllerName: String - controllingFields: [String]! - createable: Boolean! - custom: Boolean! - dataType: DataType - extraTypeInfo: FieldExtraTypeInfo - filterable: Boolean! - filteredLookupInfo: FilteredLookupInfo - highScaleNumber: Boolean! - htmlFormatted: Boolean! - inlineHelpText: String - label: String - nameField: Boolean! - polymorphicForeignKey: Boolean! - precision: Int - reference: Boolean! - referenceTargetField: String - referenceToInfos: [ReferenceToInfo]! - relationshipName: String - required: Boolean! - scale: Int - searchPrefilterable: Boolean - sortable: Boolean! - updateable: Boolean! -} - -type PicklistField implements Field { - ApiName: String! - calculated: Boolean! - compound: Boolean! - compoundComponentName: String - compoundFieldName: String - controllerName: String - controllingFields: [String]! - createable: Boolean! - custom: Boolean! - dataType: DataType - extraTypeInfo: FieldExtraTypeInfo - filterable: Boolean! - filteredLookupInfo: FilteredLookupInfo - highScaleNumber: Boolean! - htmlFormatted: Boolean! - inlineHelpText: String - label: String - nameField: Boolean! - polymorphicForeignKey: Boolean! - picklistValuesByRecordTypeIDs: [PicklistValueByRecordTypeIDs] - precision: Int - reference: Boolean! - referenceTargetField: String - referenceToInfos: [ReferenceToInfo]! - relationshipName: String - required: Boolean! - scale: Int - searchPrefilterable: Boolean - sortable: Boolean! - updateable: Boolean! -} - -type PicklistValueByRecordTypeIDs { - recordTypeID: String! - controllerValues: [ControllerValues!] - picklistValues: [PicklistValues!] - defaultValue: PicklistValues -} - -type ControllerValues { - value: String - index: Int -} - -type PicklistValues { - value: String - label: String - validFor: [Int] - attributes: PicklistAttributes -} - -type PicklistAttributes { - picklistAtrributesValueType: String - connectDisplayName: String - internalName: String -} - -# Value types implementing FieldValue -type IntValue implements FieldValue { - value: Int - displayValue: String - format: String -} - -type StringValue implements FieldValue { - value: String - displayValue: String - label: String -} - -type BooleanValue implements FieldValue { - value: Boolean - displayValue: String -} - -type IDValue implements FieldValue { - value: ID - displayValue: String -} - -type DateTimeValue implements FieldValue { - value: DateTime - displayValue: String - format: String -} - -type TimeValue implements FieldValue { - value: Time - displayValue: String - format: String -} - -type DateValue implements FieldValue { - value: Date - displayValue: String - format: String -} - -type TextAreaValue implements FieldValue { - value: TextArea - displayValue: String - label: String -} - -type LongTextAreaValue implements FieldValue { - value: LongTextArea - displayValue: String - label: String -} - -type RichTextAreaValue implements FieldValue { - value: RichTextArea - displayValue: String - label: String -} - -type PhoneNumberValue implements FieldValue { - value: PhoneNumber - displayValue: String -} - -type EmailValue implements FieldValue { - value: Email - displayValue: String -} - -type UrlValue implements FieldValue { - value: Url - displayValue: String -} - -type EncryptedStringValue implements FieldValue { - value: EncryptedString - displayValue: String -} - -type CurrencyValue implements FieldValue { - value: Currency - displayValue: String - format: String - convertCurrency: Currency -} - -type LongitudeValue implements FieldValue { - value: Longitude - displayValue: String -} - -type LatitudeValue implements FieldValue { - value: Latitude - displayValue: String -} - -type PicklistValue implements FieldValue { - value: Picklist - displayValue: String - label: String -} - -type MultiPicklistValue implements FieldValue { - value: MultiPicklist - displayValue: String - label: String -} - -type LongValue implements FieldValue { - value: Long - displayValue: String - format: String -} - -type DoubleValue implements FieldValue { - value: Double - displayValue: String - format: String -} - -type PercentValue implements FieldValue { - value: Percent - displayValue: String - format: String -} - -type Base64Value implements FieldValue { - value: Base64 - displayValue: String -} - -type JSONValue implements FieldValue { - value: JSON - displayValue: String -} - -# Aggregate types implementing FieldValue -type BooleanAggregate implements FieldValue { - value: Boolean - displayValue: String - grouping: IntValue -} - -type CurrencyAggregate implements FieldValue { - value: Currency - displayValue: String - avg: DoubleValue - count: LongValue - countDistinct: LongValue - format: String - max: CurrencyValue - min: CurrencyValue - sum: CurrencyValue -} - -type DateAggregate implements FieldValue { - value: Date - displayValue: String - calendarMonth: DateFunctionAggregation - calendarQuarter: DateFunctionAggregation - calendarYear: DateFunctionAggregation - count: LongValue - countDistinct: LongValue - dayInMonth: DateFunctionAggregation - dayInWeek: DateFunctionAggregation - dayInYear: DateFunctionAggregation - fiscalMonth: DateFunctionAggregation - fiscalQuarter: DateFunctionAggregation - fiscalYear: DateFunctionAggregation - format: String - grouping: IntValue - max: DateValue - min: DateValue - weekInMonth: DateFunctionAggregation - weekInYear: DateFunctionAggregation -} - -type DateTimeAggregate implements FieldValue { - value: DateTime - displayValue: String - calendarMonth: DateFunctionAggregation - calendarQuarter: DateFunctionAggregation - calendarYear: DateFunctionAggregation - count: LongValue - countDistinct: LongValue - dayInMonth: DateFunctionAggregation - dayInWeek: DateFunctionAggregation - dayInYear: DateFunctionAggregation - dayOnly: DateOnlyAggregation - fiscalMonth: DateFunctionAggregation - fiscalQuarter: DateFunctionAggregation - fiscalYear: DateFunctionAggregation - format: String - hourInDay: DateFunctionAggregation - max: DateTimeValue - min: DateTimeValue - weekInMonth: DateFunctionAggregation - weekInYear: DateFunctionAggregation -} - -type DoubleAggregate implements FieldValue { - value: Double - displayValue: String - avg: DoubleValue - count: LongValue - countDistinct: LongValue - format: String - max: DoubleValue - min: DoubleValue - sum: DoubleValue -} - -type EmailAggregate implements FieldValue { - value: Email - displayValue: String - count: LongValue - countDistinct: LongValue - grouping: IntValue - max: EmailValue - min: EmailValue -} - -type IDAggregate implements FieldValue { - value: ID - displayValue: String - count: LongValue - countDistinct: LongValue - grouping: IntValue - max: IDValue - min: IDValue -} - -type IntAggregate implements FieldValue { - value: Int - displayValue: String - avg: DoubleValue - count: LongValue - countDistinct: LongValue - format: String - grouping: IntValue - max: IntValue - min: IntValue - sum: LongValue -} - -type LatitudeAggregate implements FieldValue { - value: Latitude - displayValue: String - avg: DoubleValue - count: LongValue - countDistinct: LongValue - max: LatitudeValue - min: LatitudeValue - sum: DoubleValue -} - -type LongitudeAggregate implements FieldValue { - value: Longitude - displayValue: String - avg: DoubleValue - count: LongValue - countDistinct: LongValue - max: LongitudeValue - min: LongitudeValue - sum: DoubleValue -} - -type LongAggregate implements FieldValue { - value: Long - displayValue: String - avg: DoubleValue - count: LongValue - countDistinct: LongValue - format: String - grouping: IntValue - max: LongValue - min: LongValue - sum: LongValue -} - -type PercentAggregate implements FieldValue { - value: Percent - displayValue: String - avg: DoubleValue - count: LongValue - countDistinct: LongValue - format: String - max: PercentValue - min: PercentValue - sum: PercentValue -} - -type PhoneNumberAggregate implements FieldValue { - value: PhoneNumber - displayValue: String - count: LongValue - countDistinct: LongValue - grouping: IntValue - max: PhoneNumberValue - min: PhoneNumberValue -} - -type PicklistAggregate implements FieldValue { - value: Picklist - displayValue: String - count: LongValue - countDistinct: LongValue - grouping: IntValue - label: String - max: PicklistValue - min: PicklistValue -} - -type StringAggregate implements FieldValue { - value: String - displayValue: String - count: LongValue - countDistinct: LongValue - grouping: IntValue - label: String - max: StringValue - min: StringValue -} - -type TextAreaAggregate implements FieldValue { - value: TextArea - displayValue: String - count: LongValue - countDistinct: LongValue - grouping: IntValue - label: String - max: TextAreaValue - min: TextAreaValue -} - -type TimeAggregate implements FieldValue { - value: Time - displayValue: String - format: String - hourInDay: DateFunctionAggregation -} - -type UrlAggregate implements FieldValue { - value: Url - displayValue: String - count: LongValue - countDistinct: LongValue - grouping: IntValue - max: UrlValue - min: UrlValue -} - -# Helper types for aggregations -type DateFunctionAggregation { - value: Long - format: String -} - -type DateOnlyAggregation { - value: Date - format: String -} - -type CompoundField @generic { - IntValue: IntValue @fieldCategory - StringValue: StringValue @fieldCategory - BooleanValue: BooleanValue @fieldCategory - IDValue: IDValue @fieldCategory - DateTimeValue: DateTimeValue @fieldCategory - TimeValue: TimeValue @fieldCategory - DateValue: DateValue @fieldCategory - TextAreaValue: TextAreaValue @fieldCategory - LongTextAreaValue: LongTextAreaValue @fieldCategory - RichTextAreaValue: RichTextAreaValue @fieldCategory - PhoneNumberValue: PhoneNumberValue @fieldCategory - EmailValue: EmailValue @fieldCategory - UrlValue: UrlValue @fieldCategory - EncryptedStringValue: EncryptedStringValue @fieldCategory - CurrencyValue: CurrencyValue @fieldCategory - LongitudeValue: LongitudeValue @fieldCategory - LatitudeValue: LatitudeValue @fieldCategory - PicklistValue: PicklistValue @fieldCategory - MultiPicklistValue: MultiPicklistValue @fieldCategory - LongValue: LongValue @fieldCategory - DoubleValue: DoubleValue @fieldCategory - PercentValue: PercentValue @fieldCategory - Base64Value: Base64Value @fieldCategory - JSONValue: JSONValue @fieldCategory -} - -# Union types -union AnyType = BooleanValue | DateValue | DateTimeValue | DoubleValue | StringValue - -# Common input types -input ObjectInfoInput { - apiName: String! - recordTypeIDs: [ID!] - fieldNames: [String!] -} - -input OrderByClause { - order: ResultOrder - nulls: NullOrder -} - -input OrderByGeolocationClause { - distance: DistanceInput - order: ResultOrder - nulls: NullOrder -} - -input DistanceInput { - latitude: Latitude! - longitude: Longitude! -} - -input GeolocationInput { - latitude: Latitude! - longitude: Longitude! - radius: Float! - unit: Unit! -} - -input GroupByClause { - group: Boolean -} - -input GroupByDateFunction { - function: GroupByFunction -} - -input NoFunctionAggregateOrderByClause { - order: ResultsOrder - nulls: NullsOrder -} - -input AggregateOrderByNumberClause { - function: AggregateOrderByNumberFunction - order: ResultsOrder - nulls: NullsOrder -} - -input AggregateOrderByStringClause { - function: AggregateOrderByStringFunction - order: ResultsOrder - nulls: NullsOrder -} - -input DateRange { - last_n_days: Int - next_n_days: Int - last_n_weeks: Int - next_n_weeks: Int - last_n_months: Int - next_n_months: Int - last_n_quarters: Int - next_n_quarters: Int - last_n_fiscal_quarters: Int - next_n_fiscal_quarters: Int - last_n_years: Int - next_n_years: Int - last_n_fiscal_years: Int - next_n_fiscal_years: Int - n_days_ago: Int - n_weeks_ago: Int - n_months_ago: Int - n_quarters_ago: Int - n_years_ago: Int - n_fiscal_quarters_ago: Int - n_fiscal_years_ago: Int -} - -input DateInput { - value: Date - literal: DateLiteral - range: DateRange -} - -input DateTimeInput { - value: DateTime - literal: DateLiteral - range: DateRange -} - -input DatePrimitiveOperators { - eq: Date - ne: Date - lt: Date - gt: Date - lte: Date - gte: Date - in: [Date] - nin: [Date] -} - -input DateFunctionInput { - value: LongOperators - convertTimezoneValue: LongOperators -} - -input DateTimeFunctionInput { - value: DatePrimitiveOperators - convertTimezoneValue: DatePrimitiveOperators -} - -# Operator input types -input IntegerOperators { - eq: Int - ne: Int - lt: Int - gt: Int - lte: Int - gte: Int - in: [Int] - nin: [Int] -} - -input LongOperators { - eq: Long - ne: Long - lt: Long - gt: Long - lte: Long - gte: Long - in: [Long] - nin: [Long] -} - -input StringOperators { - eq: String - ne: String - like: String - lt: String - gt: String - lte: String - gte: String - in: [String] - nin: [String] -} - -input DoubleOperators { - eq: Double - ne: Double - lt: Double - gt: Double - lte: Double - gte: Double - in: [Double] - nin: [Double] -} - -input PercentOperators { - eq: Percent - ne: Percent - lt: Percent - gt: Percent - lte: Percent - gte: Percent - in: [Percent] - nin: [Percent] -} - -input LongitudeOperators { - eq: Longitude - ne: Longitude - lt: Longitude - gt: Longitude - lte: Longitude - gte: Longitude - in: [Longitude] - nin: [Longitude] -} - -input LatitudeOperators { - eq: Latitude - ne: Latitude - lt: Latitude - gt: Latitude - lte: Latitude - gte: Latitude - in: [Latitude] - nin: [Latitude] -} - -input EmailOperators { - eq: Email - ne: Email - like: Email - lt: Email - gt: Email - lte: Email - gte: Email - in: [Email] - nin: [Email] -} - -input TextAreaOperators { - eq: TextArea - ne: TextArea - like: TextArea - lt: TextArea - gt: TextArea - lte: TextArea - gte: TextArea - in: [TextArea] - nin: [TextArea] -} - -input LongTextAreaOperators { - eq: LongTextArea - ne: LongTextArea - like: LongTextArea - lt: LongTextArea - gt: LongTextArea - lte: LongTextArea - gte: LongTextArea - in: [LongTextArea] - nin: [LongTextArea] -} - -input URLOperators { - eq: Url - ne: Url - like: Url - lt: Url - gt: Url - lte: Url - gte: Url - in: [Url] - nin: [Url] -} - -input PhoneNumberOperators { - eq: PhoneNumber - ne: PhoneNumber - like: PhoneNumber - lt: PhoneNumber - gt: PhoneNumber - lte: PhoneNumber - gte: PhoneNumber - in: [PhoneNumber] - nin: [PhoneNumber] -} - -input BooleanOperators { - eq: Boolean - ne: Boolean -} - -input CurrencyOperators { - eq: Currency - ne: Currency - lt: Currency - gt: Currency - lte: Currency - gte: Currency - in: [Currency] - nin: [Currency] -} - -input TimeOperators { - eq: Time - ne: Time - lt: Time - gt: Time - lte: Time - gte: Time - in: [Time] - nin: [Time] -} - -input DateOperators { - eq: DateInput - ne: DateInput - lt: DateInput - gt: DateInput - lte: DateInput - gte: DateInput - in: [DateInput] - nin: [DateInput] - DAY_IN_WEEK: DateFunctionInput - DAY_IN_MONTH: DateFunctionInput - DAY_IN_YEAR: DateFunctionInput - WEEK_IN_MONTH: DateFunctionInput - WEEK_IN_YEAR: DateFunctionInput - CALENDAR_MONTH: DateFunctionInput - CALENDAR_QUARTER: DateFunctionInput - CALENDAR_YEAR: DateFunctionInput - FISCAL_MONTH: DateFunctionInput - FISCAL_QUARTER: DateFunctionInput - FISCAL_YEAR: DateFunctionInput -} - -input DateTimeOperators { - eq: DateTimeInput - ne: DateTimeInput - lt: DateTimeInput - gt: DateTimeInput - lte: DateTimeInput - gte: DateTimeInput - in: [DateTimeInput] - nin: [DateTimeInput] - DAY_IN_WEEK: DateFunctionInput - DAY_IN_MONTH: DateFunctionInput - DAY_IN_YEAR: DateFunctionInput - WEEK_IN_MONTH: DateFunctionInput - WEEK_IN_YEAR: DateFunctionInput - CALENDAR_MONTH: DateFunctionInput - CALENDAR_QUARTER: DateFunctionInput - CALENDAR_YEAR: DateFunctionInput - FISCAL_MONTH: DateFunctionInput - FISCAL_QUARTER: DateFunctionInput - FISCAL_YEAR: DateFunctionInput - DAY_ONLY: DateTimeFunctionInput - HOUR_IN_DAY: DateFunctionInput -} - -input PicklistOperators { - eq: Picklist - ne: Picklist - in: [Picklist] - nin: [Picklist] - like: Picklist - lt: Picklist - gt: Picklist - lte: Picklist - gte: Picklist -} - -input MultiPicklistOperators { - eq: MultiPicklist - ne: MultiPicklist - includes: [MultiPicklist] - excludes: [MultiPicklist] -} - -input GeolocationOperators { - lt: GeolocationInput - gt: GeolocationInput -} - -input IgnoreRule { - rule: String! - teamName: String! - justification: String! -} - -# Common directives -directive @generic on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT -directive @fieldCategory on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE -directive @category(name: String!) on FIELD -"Specifies the team name for a type" -directive @team(name: String!) on OBJECT | INPUT_OBJECT | INTERFACE | ENUM | UNION | SCALAR -"Specifies the validation rules to ignore for this element." -directive @ignoreRule(rules: [IgnoreRule!]!) on OBJECT - | INPUT_OBJECT - | INTERFACE - | ENUM - | UNION - | SCALAR - | FIELD_DEFINITION - | INPUT_FIELD_DEFINITION - | ARGUMENT_DEFINITION - | ENUM_VALUE -directive @optional on FIELD diff --git a/skills/using-webapp-salesforce-data/SKILL.md b/skills/using-webapp-salesforce-data/SKILL.md new file mode 100644 index 0000000..cd88681 --- /dev/null +++ b/skills/using-webapp-salesforce-data/SKILL.md @@ -0,0 +1,363 @@ +--- +name: using-webapp-salesforce-data +description: "Salesforce data access for reading, writing, and querying records via REST, GraphQL, Apex, or Platform SDK. Use when the user wants to fetch, search, filter, sort, display, create, update, delete, or attach files to Salesforce records (standard objects like Accounts, Contacts, Opportunities, Cases, Quotes, or any custom object) in a web app or UI component (React, Angular, Vue, etc.); call Chatter, Connect, or Apex REST APIs; or invoke AuraEnabled Apex methods from an external app. Does not apply to authentication/OAuth setup, schema changes (adding fields, relationships), Bulk/Tooling/Metadata API usage, declarative automation (Flows, Process Builder), general LWC/Apex coding guidance without a specific data operation, or Salesforce admin/configuration tasks." +--- + +# Salesforce Data Access + +## When to Use + +Use this skill when the user wants to: + +- **Fetch or display Salesforce data** — Query records (Account, Contact, Opportunity, custom objects) to show in a component +- **Create, update, or delete records** — Perform mutations on Salesforce data +- **Add data fetching to a component** — Wire up a React component to Salesforce data +- **Call REST APIs** — Use Connect REST, Apex REST, or UI API endpoints +- **Explore the org schema** — Discover available objects, fields, or relationships + +## Data SDK Requirement + +> **All Salesforce data access MUST use the Data SDK** (`@salesforce/sdk-data`). The SDK handles authentication, CSRF, and base URL resolution. Never use `fetch()` or `axios` directly. + +```typescript +import { createDataSDK, gql } from "@salesforce/sdk-data"; + +const sdk = await createDataSDK(); + +// GraphQL for record queries/mutations (PREFERRED) +const response = await sdk.graphql?.(query, variables); + +// REST for Connect REST, Apex REST, UI API (when GraphQL insufficient) +const res = await sdk.fetch?.("/services/apexrest/my-resource"); +``` + +**Always use optional chaining** (`sdk.graphql?.()`, `sdk.fetch?.()`) — these methods may be undefined in some surfaces. + +## Supported APIs + +**Only the following APIs are permitted.** Any endpoint not listed here must not be used. + +| API | Method | Endpoints / Use Case | +|-----|--------|----------------------| +| GraphQL | `sdk.graphql` | All record queries and mutations via `uiapi { }` namespace | +| UI API REST | `sdk.fetch` | `/services/data/v{ver}/ui-api/records/{id}` — record metadata when GraphQL is insufficient | +| Apex REST | `sdk.fetch` | `/services/apexrest/{resource}` — custom server-side logic, aggregates, multi-step transactions | +| Connect REST | `sdk.fetch` | `/services/data/v{ver}/connect/file/upload/config` — file upload config | +| Einstein LLM | `sdk.fetch` | `/services/data/v{ver}/einstein/llm/prompt/generations` — AI text generation | + +**Not supported:** + +- **Enterprise REST query endpoint** (`/services/data/v*/query` with SOQL) — blocked at the proxy level. Use GraphQL for record reads; use Apex REST if server-side SOQL aggregates are required. +- **Aura-enabled Apex** (`@AuraEnabled`) — an LWC/Aura pattern with no invocation path from React webapps. +- **Chatter API** (`/chatter/users/me`) — use `uiapi { currentUser { ... } }` in a GraphQL query instead. +- **Any other Salesforce REST endpoint** not listed in the supported table above. + +## Decision: GraphQL vs REST + +| Need | Method | Example | +|------|--------|---------| +| Query/mutate records | `sdk.graphql` | Account, Contact, custom objects | +| Current user info | `sdk.graphql` | `uiapi { currentUser { Id Name { value } } }` | +| UI API record metadata | `sdk.fetch` | `/ui-api/records/{id}` | +| Connect REST | `sdk.fetch` | `/connect/file/upload/config` | +| Apex REST | `sdk.fetch` | `/services/apexrest/auth/login` | +| Einstein LLM | `sdk.fetch` | `/einstein/llm/prompt/generations` | + +**GraphQL is preferred** for record operations. Use REST only when GraphQL doesn't cover the use case. + +--- + +## GraphQL Workflow + +### Step 1: Acquire Schema + +The `schema.graphql` file (265K+ lines) is the source of truth. **Never open or parse it directly.** + +1. Check if `schema.graphql` exists at the SFDX project root +2. If missing, run from the **webapp dir**: `npm run graphql:schema` +3. Custom objects appear only after metadata is deployed + +### Step 2: Look Up Entity Schema + +Map user intent to PascalCase names ("accounts" → `Account`), then **run the search script from the project root**: + +```bash +# From project root — look up all relevant schema info for one or more entities +bash .a4drules/skills/using-salesforce-data/graphql-search.sh Account + +# Multiple entities at once +bash .a4drules/skills/using-salesforce-data/graphql-search.sh Account Contact Opportunity +``` + +The script outputs five sections per entity: +1. **Type definition** — all queryable fields and relationships +2. **Filter options** — available fields for `where:` conditions +3. **Sort options** — available fields for `orderBy:` +4. **Create input** — fields accepted by create mutations +5. **Update input** — fields accepted by update mutations + +Use this output to determine exact field names before writing any query or mutation. **Maximum 2 script runs.** If the entity still can't be found, ask the user — the object may not be deployed. + +### Step 3: Generate Query + +Use the templates below. Every field name **must** be verified from the script output in Step 2. + +#### Read Query Template + +```graphql +query GetAccounts { + uiapi { + query { + Account(where: { Industry: { eq: "Technology" } }, first: 10) { + edges { + node { + Id + Name @optional { value } + Industry @optional { value } + # Parent relationship + Owner @optional { Name { value } } + # Child relationship + Contacts @optional { + edges { node { Name @optional { value } } } + } + } + } + } + } + } +} +``` + +**FLS Resilience**: Apply `@optional` to all record fields. The server omits inaccessible fields instead of failing. Consuming code must use optional chaining: + +```typescript +const name = node.Name?.value ?? ""; +``` + +#### Mutation Template + +```graphql +mutation CreateAccount($input: AccountCreateInput!) { + uiapi { + AccountCreate(input: $input) { + Record { Id Name { value } } + } + } +} +``` + +**Mutation constraints:** +- Create: Include required fields, only `createable` fields, no child relationships +- Update: Include `Id`, only `updateable` fields +- Delete: Include `Id` only + +#### Object Metadata & Picklist Values + +Use `uiapi { objectInfos(...) }` to fetch field metadata or picklist values. Pass **either** `apiNames` or `objectInfoInputs` — never both in the same query. + +**Object metadata** (field labels, data types, CRUD flags): + +```typescript +const GET_OBJECT_INFO = gql` + query GetObjectInfo($apiNames: [String!]!) { + uiapi { + objectInfos(apiNames: $apiNames) { + ApiName + label + labelPlural + fields { + ApiName + label + dataType + updateable + createable + } + } + } + } +`; + +const sdk = await createDataSDK(); +const response = await sdk.graphql?.(GET_OBJECT_INFO, { apiNames: ["Account"] }); +const objectInfos = response?.data?.uiapi?.objectInfos ?? []; +``` + +**Picklist values** (use `objectInfoInputs` + `... on PicklistField` inline fragment): + +```typescript +const GET_PICKLIST_VALUES = gql` + query GetPicklistValues($objectInfoInputs: [ObjectInfoInput!]!) { + uiapi { + objectInfos(objectInfoInputs: $objectInfoInputs) { + ApiName + fields { + ApiName + ... on PicklistField { + picklistValuesByRecordTypeIDs { + recordTypeID + picklistValues { + label + value + } + } + } + } + } + } + } +`; + +const response = await sdk.graphql?.(GET_PICKLIST_VALUES, { + objectInfoInputs: [{ objectApiName: "Account" }], +}); +const fields = response?.data?.uiapi?.objectInfos?.[0]?.fields ?? []; +``` + +### Step 4: Validate & Test + +1. **Lint**: `npx eslint ` from webapp dir +2. **Test**: Ask user before testing. For mutations, request input values — never fabricate data. + +**If ESLint reports a GraphQL error** (e.g. `Cannot query field`, `Unknown type`, `Unknown argument`), the field or type name is wrong. Re-run the schema search script to find the correct name — do not guess: + +```bash +# From project root — re-check the entity that caused the error +bash .a4drules/skills/using-salesforce-data/graphql-search.sh +``` + +Then fix the query using the exact names from the script output. + +--- + +## Webapp Integration (React) + +```typescript +import { createDataSDK, gql } from "@salesforce/sdk-data"; + +const GET_ACCOUNTS = gql` + query GetAccounts { + uiapi { + query { + Account(first: 10) { + edges { + node { + Id + Name @optional { value } + Industry @optional { value } + } + } + } + } + } + } +`; + +const sdk = await createDataSDK(); +const response = await sdk.graphql?.(GET_ACCOUNTS); + +if (response?.errors?.length) { + throw new Error(response.errors.map(e => e.message).join("; ")); +} + +const accounts = response?.data?.uiapi?.query?.Account?.edges?.map(e => e.node) ?? []; +``` + +--- + +## REST API Patterns + +Use `sdk.fetch` when GraphQL is insufficient. See the [Supported APIs](#supported-apis) table for the full allowlist. + +```typescript +declare const __SF_API_VERSION__: string; +const API_VERSION = typeof __SF_API_VERSION__ !== "undefined" ? __SF_API_VERSION__ : "65.0"; + +// Connect — file upload config +const res = await sdk.fetch?.(`/services/data/v${API_VERSION}/connect/file/upload/config`); + +// Apex REST (no version in path) +const res = await sdk.fetch?.("/services/apexrest/auth/login", { + method: "POST", + body: JSON.stringify({ email, password }), + headers: { "Content-Type": "application/json" }, +}); + +// UI API — record with metadata (prefer GraphQL for simple reads) +const res = await sdk.fetch?.(`/services/data/v${API_VERSION}/ui-api/records/${recordId}`); + +// Einstein LLM +const res = await sdk.fetch?.(`/services/data/v${API_VERSION}/einstein/llm/prompt/generations`, { + method: "POST", + body: JSON.stringify({ promptTextorId: prompt }), +}); +``` + +**Current user**: Do not use Chatter (`/chatter/users/me`). Use GraphQL instead: + +```typescript +const GET_CURRENT_USER = gql` + query CurrentUser { + uiapi { currentUser { Id Name { value } } } + } +`; +const response = await sdk.graphql?.(GET_CURRENT_USER); +``` + +--- + +## Directory Structure + +``` +/ ← SFDX project root +├── schema.graphql ← grep target (lives here) +├── sfdx-project.json +└── force-app/main/default/webapplications// ← webapp dir + ├── package.json ← npm scripts + └── src/ +``` + +| Command | Run From | Why | +|---------|----------|-----| +| `npm run graphql:schema` | webapp dir | Script in webapp's package.json | +| `npx eslint ` | webapp dir | Reads eslint.config.js | +| `bash .a4drules/skills/using-salesforce-data/graphql-search.sh ` | project root | Schema lookup | +| `sf api request rest` | project root | Needs sfdx-project.json | + +--- + +## Quick Reference + +### Schema Lookup (from project root) + +Run the search script to get all relevant schema info in one step: + +```bash +bash .a4drules/skills/using-salesforce-data/graphql-search.sh +``` + +| Script Output Section | Used For | +|-----------------------|----------| +| Type definition | Field names, parent/child relationships | +| Filter options | `where:` conditions | +| Sort options | `orderBy:` | +| CreateRepresentation | Create mutation field list | +| UpdateRepresentation | Update mutation field list | + +### Error Categories + +| Error Contains | Resolution | +|----------------|------------| +| `Cannot query field` | Field name is wrong — run `graphql-search.sh ` and use the exact name from the Type definition section | +| `Unknown type` | Type name is wrong — run `graphql-search.sh ` to confirm the correct PascalCase entity name | +| `Unknown argument` | Argument name is wrong — run `graphql-search.sh ` and check Filter or OrderBy sections | +| `invalid syntax` | Fix syntax per error message | +| `validation error` | Field name is wrong — run `graphql-search.sh ` to verify | +| `VariableTypeMismatch` | Correct argument type from schema | +| `invalid cross reference id` | Entity deleted — ask for valid Id | + +### Checklist + +- [ ] All field names verified via search script (Step 2) +- [ ] `@optional` applied to record fields (reads) +- [ ] Optional chaining in consuming code +- [ ] Lint passes: `npx eslint ` diff --git a/skills/using-webapp-salesforce-data/graphql-search.sh b/skills/using-webapp-salesforce-data/graphql-search.sh new file mode 100644 index 0000000..28f923c --- /dev/null +++ b/skills/using-webapp-salesforce-data/graphql-search.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +# graphql-search.sh — Look up one or more Salesforce entities in schema.graphql. +# +# Run from the SFDX project root (where schema.graphql lives): +# bash .a4drules/skills/using-salesforce-data/graphql-search.sh Account +# bash .a4drules/skills/using-salesforce-data/graphql-search.sh Account Contact Opportunity +# +# Pass a custom schema path with -s / --schema: +# bash .a4drules/skills/using-salesforce-data/graphql-search.sh -s /path/to/schema.graphql Account +# bash .a4drules/skills/using-salesforce-data/graphql-search.sh --schema ./other/schema.graphql Account Contact +# +# Output sections per entity: +# 1. Type definition — all fields and relationships +# 2. Filter options — _Filter input (for `where:`) +# 3. Sort options — _OrderBy input (for `orderBy:`) +# 4. Create input — CreateRepresentation (for create mutations) +# 5. Update input — UpdateRepresentation (for update mutations) + +SCHEMA="./schema.graphql" + +# ── Argument parsing ───────────────────────────────────────────────────────── + +while [[ $# -gt 0 ]]; do + case "$1" in + -s|--schema) + if [[ -z "${2-}" || "$2" == -* ]]; then + echo "ERROR: --schema requires a file path argument" + exit 1 + fi + SCHEMA="$2" + shift 2 + ;; + --) + shift + break + ;; + -*) + echo "ERROR: Unknown option: $1" + echo "Usage: bash $0 [-s ] [EntityName2 ...]" + exit 1 + ;; + *) + break + ;; + esac +done + +if [ $# -eq 0 ]; then + echo "Usage: bash $0 [-s ] [EntityName2 ...]" + echo "Example: bash $0 Account" + echo "Example: bash $0 Account Contact Opportunity" + echo "Example: bash $0 --schema /path/to/schema.graphql Account" + exit 1 +fi + +if [ ! -f "$SCHEMA" ]; then + echo "ERROR: schema.graphql not found at $SCHEMA" + echo " Make sure you are running from the SFDX project root, or pass the path explicitly:" + echo " bash $0 --schema " + echo " If the file is missing entirely, generate it from the webapp dir:" + echo " cd force-app/main/default/webapplications/ && npm run graphql:schema" + exit 1 +fi + +# ── Helper: extract lines from a grep match through the closing brace ──────── +# Prints up to MAX_LINES lines after (and including) the first match of PATTERN. +# Uses a generous line count — blocks are always closed by a "}" line. + +extract_block() { + local label="$1" + local pattern="$2" + local max_lines="$3" + + local match + match=$(grep -nE "$pattern" "$SCHEMA" | head -1) + + if [ -z "$match" ]; then + echo " (not found: $pattern)" + return + fi + + echo "### $label" + grep -E "$pattern" "$SCHEMA" -A "$max_lines" | \ + awk '/^\}$/{print; exit} {print}' | \ + head -n "$max_lines" + echo "" +} + +# ── Main loop ──────────────────────────────────────────────────────────────── + +for ENTITY in "$@"; do + echo "" + echo "======================================================================" + echo " SCHEMA LOOKUP: $ENTITY" + echo "======================================================================" + echo "" + + # 1. Type definition — all fields and relationships + extract_block \ + "Type definition — fields and relationships" \ + "^type ${ENTITY} implements Record" \ + 200 + + # 2. Filter input — used in `where:` arguments + extract_block \ + "Filter options — use in where: { ... }" \ + "^input ${ENTITY}_Filter" \ + 100 + + # 3. OrderBy input — used in `orderBy:` arguments + extract_block \ + "Sort options — use in orderBy: { ... }" \ + "^input ${ENTITY}_OrderBy" \ + 60 + + # 4. Create mutation inputs + extract_block \ + "Create mutation wrapper — ${ENTITY}CreateInput" \ + "^input ${ENTITY}CreateInput" \ + 10 + + extract_block \ + "Create mutation fields — ${ENTITY}CreateRepresentation" \ + "^input ${ENTITY}CreateRepresentation" \ + 100 + + # 5. Update mutation inputs + extract_block \ + "Update mutation wrapper — ${ENTITY}UpdateInput" \ + "^input ${ENTITY}UpdateInput" \ + 10 + + extract_block \ + "Update mutation fields — ${ENTITY}UpdateRepresentation" \ + "^input ${ENTITY}UpdateRepresentation" \ + 100 + + echo "" +done