{"record":{"id":"9b5455f8919c5eb6","repo":"supabase/supabase","slug":"app-id-is-required","errorCode":null,"errorMessage":"App ID is required","messagePattern":"App ID is required","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/studio/data/oauth/authorized-app-revoke-mutation.ts","lineNumber":14,"sourceCode":"import { useMutation, useQueryClient } from '@tanstack/react-query'\nimport { toast } from 'sonner'\n\nimport { oauthAppKeys } from './keys'\nimport { handleError, post } from '@/data/fetchers'\nimport type { ResponseError, UseCustomMutationOptions } from '@/types'\n\nexport type AuthorizedAppRevokeVariables = {\n  id: string\n  orgSlug: string\n}\n\nexport async function revokeAuthorizedApp({ id, orgSlug: slug }: AuthorizedAppRevokeVariables) {\n  if (!id) throw new Error('App ID is required')\n  if (!slug) throw new Error('Organization slug is required')\n\n  const { data, error } = await post('/platform/organizations/{slug}/oauth/apps/{id}/revoke', {\n    params: { path: { slug, id } },\n  })\n\n  if (error) handleError(error)\n  return data\n}\n\ntype AuthorizedAppRevokeData = Awaited<ReturnType<typeof revokeAuthorizedApp>>\n\nexport const useAuthorizedAppRevokeMutation = ({\n  onSuccess,\n  onError,\n  ...options\n}: Omit<\n  UseCustomMutationOptions<AuthorizedAppRevokeData, ResponseError, AuthorizedAppRevokeVariables>,","sourceCodeStart":1,"sourceCodeEnd":32,"githubUrl":"https://github.com/supabase/supabase/blob/beee91b9c2228dd57302dec75c733baaa84ab543/apps/studio/data/oauth/authorized-app-revoke-mutation.ts#L1-L32","documentation":"Thrown by revokeAuthorizedApp when the id of the authorized OAuth app is missing before POSTing to /platform/organizations/{slug}/oauth/apps/{id}/revoke. The path requires a concrete app id to identify which authorization to revoke.","triggerScenarios":"Revoke mutation fired from an account/integrations list row where the row's app id is undefined, or a programmatic revoke called without an id. The mutation destructures { id, orgSlug } and bails before the network call.","commonSituations":"List row key uses index instead of id and the row data is partially loaded; the revoke menu opens before the apps query returns; a stale ref holds an id that was cleared on unmount.","solutions":["Disable the Revoke action on rows where id is missing.","Pass the row's id explicitly from the mapped list item rather than a selected-state that may be null.","Add an early return / toast in the click handler if !id."],"exampleFix":"// before\nconst onRevoke = (app) => revoke({ id: app?.id, orgSlug })\n\n// after\nconst onRevoke = (app) => {\n  if (!app?.id) return\n  revoke({ id: app.id, orgSlug })\n}","handlingStrategy":"validation","validationCode":"const onRevoke = (app?: { id?: string }) => {\n  if (!app?.id) {\n    toast.error('Cannot identify the app to revoke.')\n    return\n  }\n  revoke({ id: app.id, orgSlug })\n}","typeGuard":"const hasAppId = (\n  v: { id?: string }\n): v is { id: string } => typeof v.id === 'string' && v.id.length > 0","tryCatchPattern":"try {\n  await revokeAsync({ id: id!, orgSlug })\n} catch (e) {\n  if (e instanceof Error && e.message === 'App ID is required') {\n    toast.error('App not loaded yet. Refresh and try again.')\n  } else {\n    throw e\n  }\n}","preventionTips":["Bind the revoke action to a concrete row id, not a possibly-null selection.","Disable the revoke control on rows whose id has not loaded.","Pass the full app object into the handler so id is always in scope."],"tags":["validation","precondition","oauth","app-id","mutation"],"backgroundTag":null,"analyzedSha":"beee91b9c2228dd57302dec75c733baaa84ab543","analyzedAt":"2026-08-12T06:51:48.935Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}