{"record":{"id":"6e7de19c20df7959","repo":"supabase/supabase","slug":"content-id-is-not-a-notebook-got-type-data","errorCode":null,"errorMessage":"Content ${id} is not a notebook (got type: ${data.type})","messagePattern":"Content (.+?) is not a notebook \\(got type: (.+?)\\)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/studio/data/content/notebooks/notebook-query.ts","lineNumber":20,"sourceCode":"\nimport { getContentById } from '../content-id-query'\nimport { contentKeys } from '../keys'\nimport type { Notebooks, ResponseError, UseCustomQueryOptions } from '@/types'\n\nexport type NotebookVariables = { projectRef?: string; id?: string }\nexport type NotebookError = ResponseError\n\nexport async function getNotebook(\n  { projectRef, id }: NotebookVariables,\n  signal?: AbortSignal,\n  headers?: HeadersInit\n) {\n  const data = await getContentById({ projectRef, id }, signal, headers)\n\n  // api-types doesn't have 'notebook' in GetUserContentByIdResponse['type'] yet — same gap\n  // tracked by the ContentBase TODO in content-query.ts — so this narrowing can't be static.\n  if ((data.type as string) !== 'notebook') {\n    throw new Error(`Content ${id} is not a notebook (got type: ${data.type})`)\n  }\n\n  return data as unknown as Omit<typeof data, 'type' | 'content'> & {\n    type: 'notebook'\n    content: Notebooks.Content\n  }\n}\n\nexport type NotebookData = Awaited<ReturnType<typeof getNotebook>>\n\nexport const useNotebookQuery = <TData = NotebookData>(\n  { projectRef, id }: NotebookVariables,\n  { enabled = true, ...options }: UseCustomQueryOptions<NotebookData, NotebookError, TData> = {}\n) =>\n  useQuery<NotebookData, NotebookError, TData>({\n    queryKey: contentKeys.resource(projectRef, id),\n    queryFn: ({ signal }) => getNotebook({ projectRef, id }, signal),\n    enabled: enabled && typeof projectRef !== 'undefined' && typeof id !== 'undefined',","sourceCodeStart":2,"sourceCodeEnd":38,"githubUrl":"https://github.com/supabase/supabase/blob/beee91b9c2228dd57302dec75c733baaa84ab543/apps/studio/data/content/notebooks/notebook-query.ts#L2-L38","documentation":"Thrown by getNotebook (apps/studio/data/content/notebooks/notebook-query.ts:20) after a successful getContentById call, when the returned content item's type is not 'notebook'. Unlike the ref/id guards, this fires after the fetch succeeds: it is a runtime type-narrowing failure, not a precondition. The api-types package does not yet include 'notebook' in the content type union (tracked by the ContentBase TODO), so the check is dynamic via `(data.type as string) !== 'notebook'`.","triggerScenarios":"Loading a content id that exists but is a sql/report/log_sql snippet (not a notebook) through the notebook loader, e.g. a deep link to /notebook/{id} where the id actually belongs to a SQL snippet.","commonSituations":"Stale or shared URL pointing a notebook route at a non-notebook item, a content item whose type was changed, or the SQL editor and notebook viewer sharing an id namespace and routing incorrectly.","solutions":["Validate the content type before routing: fetch the item via getContentById and branch on data.type before entering the notebook view.","In the notebook route, treat a non-notebook type as a 404/redirect to the correct viewer (SQL editor for 'sql', report viewer for 'report').","Once api-types adds 'notebook' to the union, replace the dynamic cast with a static type guard."],"exampleFix":"// before\nconst data = await getNotebook({ projectRef, id })\n\n// after\nconst raw = await getContentById({ projectRef, id })\nif ((raw.type as string) !== 'notebook') {\n  // route to the correct viewer or show a not-found state\n  return null\n}\nconst data = raw as unknown as NotebookData","handlingStrategy":"type-guard","validationCode":"const raw = await getContentById({ projectRef, id })\nif ((raw.type as string) !== 'notebook') {\n  // not a notebook — do not call getNotebook\n  return null\n}","typeGuard":"const isNotebook = (\n  c: { type: string }\n): c is { type: 'notebook' } => (c.type as string) === 'notebook'","tryCatchPattern":"try {\n  const nb = await getNotebook({ projectRef, id })\n} catch (e) {\n  if (e instanceof Error && /is not a notebook/.test(e.message)) {\n    // redirect to the correct viewer or show not-found\n  } else {\n    throw e\n  }\n}","preventionTips":["Validate content type via getContentById before routing into the notebook viewer.","Treat a non-notebook id on a notebook route as a redirect to the correct viewer (sql -> SQL editor).","Once api-types adds 'notebook' to the content union, replace the dynamic cast with a static type guard."],"tags":["type-narrowing","content-type","notebook","runtime-check","api-types","studio"],"backgroundTag":null,"analyzedSha":"beee91b9c2228dd57302dec75c733baaa84ab543","analyzedAt":"2026-08-12T06:51:48.935Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}