mirror of
https://github.com/supabase/supabase.git
synced 2026-07-02 21:34:18 +08:00
* added initial queue operations and feature flag * updated types * added dirty state tracking on columns * updated queue operations * updated operation types and queue * updated spacing * removed on cancel * updated to support saving * updated to include eye details * updated spacing for orders * updated to support shortcuts * added feature preview * updated to unify queue methods * added key generation * used unique keys rather than random uuid * updated based on code review * operation key * updated handle cancel * updated remove operation button * updated views for toast * updated logic to support optimistic updates * updated types * code cleanup: remove LLM slop * updated PR bug * updated preview for logout * updated based on code review * removed use effect as it was causing problems * fixed toast mounting away from sql editor * removed toast for dedicated action bar * cleaned up logic * updated queue operations * renamed method * updated name for types * updated comment * fixed code rabbit solution * added check for changed column * added tests
127 lines
4.0 KiB
TypeScript
127 lines
4.0 KiB
TypeScript
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
import { toast } from 'sonner'
|
|
|
|
import { executeSql } from 'data/sql/execute-sql-query'
|
|
import { wrapWithTransaction } from 'data/sql/utils/transaction'
|
|
import { RoleImpersonationState, wrapWithRoleImpersonation } from 'lib/role-impersonation'
|
|
import { isRoleImpersonationEnabled } from 'state/role-impersonation-state'
|
|
import {
|
|
EditCellContentPayload,
|
|
QueuedOperation,
|
|
QueuedOperationType,
|
|
} from 'state/table-editor-operation-queue.types'
|
|
import type { ResponseError, UseCustomMutationOptions } from 'types'
|
|
import { tableRowKeys } from './keys'
|
|
import { getTableRowUpdateSql } from './table-row-update-mutation'
|
|
|
|
export type OperationQueueSaveVariables = {
|
|
projectRef: string
|
|
connectionString?: string | null
|
|
operations: readonly QueuedOperation[]
|
|
roleImpersonationState?: RoleImpersonationState
|
|
}
|
|
|
|
/**
|
|
* Generates SQL for a single queued operation.
|
|
* Extend this function as new operation types are added.
|
|
*/
|
|
function getOperationSql(operation: QueuedOperation): string {
|
|
switch (operation.type) {
|
|
case QueuedOperationType.EDIT_CELL_CONTENT: {
|
|
const payload = operation.payload as EditCellContentPayload
|
|
return getTableRowUpdateSql({
|
|
table: {
|
|
id: payload.table.id,
|
|
name: payload.table.name,
|
|
schema: payload.table.schema,
|
|
},
|
|
configuration: { identifiers: payload.rowIdentifiers },
|
|
payload: { [payload.columnName]: payload.newValue },
|
|
enumArrayColumns: payload.enumArrayColumns ?? [],
|
|
returning: false,
|
|
})
|
|
}
|
|
default:
|
|
throw new Error(`Unknown operation type: ${(operation as QueuedOperation).type}`)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Saves all queued operations in a single database transaction.
|
|
* If any operation fails, the entire transaction is rolled back.
|
|
*/
|
|
export async function saveOperationQueue({
|
|
projectRef,
|
|
connectionString,
|
|
operations,
|
|
roleImpersonationState,
|
|
}: OperationQueueSaveVariables) {
|
|
if (operations.length === 0) {
|
|
return { result: [] }
|
|
}
|
|
|
|
// Generate SQL for each operation, stripping trailing semicolons to avoid double semicolons when joining
|
|
const statements = operations.map((op) => {
|
|
const sql = getOperationSql(op)
|
|
return sql.endsWith(';') ? sql.slice(0, -1) : sql
|
|
})
|
|
|
|
// Combine all statements into a single transaction
|
|
const transactionSql = wrapWithTransaction(statements.join(';\n') + ';')
|
|
|
|
// Wrap with role impersonation if enabled
|
|
const sql = wrapWithRoleImpersonation(transactionSql, roleImpersonationState)
|
|
|
|
const { result } = await executeSql({
|
|
projectRef,
|
|
connectionString,
|
|
sql,
|
|
isRoleImpersonationEnabled: isRoleImpersonationEnabled(roleImpersonationState?.role),
|
|
queryKey: ['operation-queue-save'],
|
|
})
|
|
|
|
return { result }
|
|
}
|
|
|
|
type OperationQueueSaveData = Awaited<ReturnType<typeof saveOperationQueue>>
|
|
|
|
export const useOperationQueueSaveMutation = ({
|
|
onSuccess,
|
|
onError,
|
|
...options
|
|
}: Omit<
|
|
UseCustomMutationOptions<OperationQueueSaveData, ResponseError, OperationQueueSaveVariables>,
|
|
'mutationFn'
|
|
> = {}) => {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation<OperationQueueSaveData, ResponseError, OperationQueueSaveVariables>({
|
|
mutationFn: (vars) => saveOperationQueue(vars),
|
|
async onSuccess(data, variables, context) {
|
|
const { projectRef, operations } = variables
|
|
|
|
// Collect all unique table IDs that were affected
|
|
const affectedTableIds = [...new Set(operations.map((op) => op.tableId))]
|
|
|
|
// Invalidate queries for all affected tables (both rows and count)
|
|
await Promise.all(
|
|
affectedTableIds.map((tableId) =>
|
|
queryClient.invalidateQueries({
|
|
queryKey: tableRowKeys.tableRowsAndCount(projectRef, tableId),
|
|
})
|
|
)
|
|
)
|
|
|
|
await onSuccess?.(data, variables, context)
|
|
},
|
|
async onError(data, variables, context) {
|
|
if (onError === undefined) {
|
|
toast.error(`Failed to save changes: ${data.message}`)
|
|
} else {
|
|
onError(data, variables, context)
|
|
}
|
|
},
|
|
...options,
|
|
})
|
|
}
|