Files
supabase/apps/studio/data/content/notebooks/notebook-upsert-mutation.ts
Charis 4dee589735 fix(studio): stop assistant fabricating database_identifier for notebooks (#49558)
## Summary

* Fixes
[FE-4275](https://linear.app/supabase/issue/FE-4275/assistant-always-creates-notebooks-with-wrong-identifier-first-try):
the assistant always created database notebook cells with a fabricated
`database_identifier` (`"primary"`, later observed as `""` /
`"_primary"` under different prompt wording) instead of omitting the key
for the project's primary database, which tripped the tool's
reject-and-retry validation on the very first attempt.
* Prompt wording alone wasn't reliable — live eval runs against the real
model kept substituting a new placeholder every time the prompt was
tightened further.
* Normalizes an empty-string `database_identifier` to absent at the
schema level (`databaseIdentifierSchema` in `notebook-schema.ts`), which
is inherited by every schema built from it — the AI SDK's `inputSchema`
for `create_notebook`/`update_notebook`, and the write-boundary
`writableNotebookSchema` used right before the PUT to the backend.
* Adds an eval case (`evals/dataset.ts`) reproducing the original bug,
plus unit tests covering schema-level and write-boundary normalization.

## Test plan

- [X] `pnpm --filter studio exec tsc --noEmit` passes
- [X] `pnpm exec prettier --check` passes on touched files
- [X] Unit tests pass: `notebook-schema.test.ts`,
`notebook-upsert-mutation.test.ts`, `notebook-tools.test.ts` (104 tests)
- [X] Ran the new eval case against the real model 3x before the code
fix (0% correctness, fabricated `""`/`"_primary"`) and 3x after (100%
correctness)

## Summary by CodeRabbit

* **Bug Fixes**
* Improved notebook handling of empty database identifiers by treating
them as absent.
  * Ensured notebook requests omit unused database identifier fields.
  * Added validation guidance for read-replica database identifiers.
2026-08-25 15:08:28 -04:00

103 lines
2.9 KiB
TypeScript

import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { upsertContent, type UpsertContentPayload } from '../content-upsert-mutation'
import { contentKeys } from '../keys'
import { writableNotebookSchema, type WritableNotebook } from './notebook-schema'
import type { ResponseError, UseCustomMutationOptions } from '@/types'
function buildNotebookUpsertPayload({
id,
name,
description,
content,
}: {
id: string
name: string
description?: string
content: WritableNotebook
}): UpsertContentPayload {
return {
id,
name,
description,
type: 'notebook',
visibility: 'project',
content: writableNotebookSchema.parse(content),
}
}
export type CreateNotebookVariables = {
projectRef: string
name: string
description?: string
content: WritableNotebook
}
export async function createNotebook(
{ projectRef, name, description, content }: CreateNotebookVariables,
signal?: AbortSignal,
headersInit?: HeadersInit
) {
const id = crypto.randomUUID()
const payload = buildNotebookUpsertPayload({ id, name, description, content })
await upsertContent({ projectRef, payload }, signal, headersInit)
return { id }
}
export type CreateNotebookData = Awaited<ReturnType<typeof createNotebook>>
export type UpsertNotebookVariables = {
id: string
projectRef: string
name: string
description?: string
content: WritableNotebook
}
export async function upsertNotebook(
{ projectRef, id, name, description, content }: UpsertNotebookVariables,
signal?: AbortSignal,
headersInit?: HeadersInit
) {
const payload = buildNotebookUpsertPayload({ id, name, description, content })
return upsertContent({ projectRef, payload }, signal, headersInit)
}
export type UpdateNotebookData = Awaited<ReturnType<typeof upsertNotebook>>
export const useUpsertNotebookMutation = ({
onError,
onSuccess,
...options
}: Omit<
UseCustomMutationOptions<UpdateNotebookData, ResponseError, UpsertNotebookVariables>,
'mutationFn'
> = {}) => {
const queryClient = useQueryClient()
return useMutation<UpdateNotebookData, ResponseError, UpsertNotebookVariables>({
mutationFn: (args) => upsertNotebook(args),
async onSuccess(data, variables, context) {
const { projectRef, id } = variables
await Promise.all([
queryClient.invalidateQueries({ queryKey: contentKeys.allContentLists(projectRef) }),
queryClient.invalidateQueries({ queryKey: contentKeys.infiniteList(projectRef) }),
queryClient.invalidateQueries({ queryKey: contentKeys.resource(projectRef, id) }),
])
await onSuccess?.(data, variables, context)
},
async onError(error, variables, context) {
if (onError === undefined) {
toast.error(`Failed to update notebook: ${error.message}`)
} else {
onError(error, variables, context)
}
},
...options,
})
}