working sql editor upsert

This commit is contained in:
Alaister Young committed 2023-07-18 13:06:09 +08:00
1 parent 10f5640837
commit da6ff0f2b0
7 files changed
+405 -46

No files matched your search

@@ -75,12 +75,14 @@ const CommandMenuWrapper = ({ children }: PropsWithChildren<{}>) => {
try {
const snippet = createSqlSnippetSkeleton({
id: uuidv4(),
name: title || 'Generated query',
owner_id: profile?.id,
project_id: selectedProject?.id,
sql: formattedSql,
})
const data = { ...snippet, id: uuidv4() }
snap.addSnippet(data as SqlSnippet, ref)
snap.addSnippet(snippet as SqlSnippet, ref)
ui.setNotification({
category: 'success',
message: `Successfully saved snippet!`,
@@ -15,21 +15,27 @@ export const getResultsMarkdown = (results: any[]) => {
}
export const createSqlSnippetSkeleton = ({
id,
name,
sql,
owner_id,
project_id,
}: {
id?: string
name?: string
sql?: string
owner_id?: number
project_id?: number
} = {}): UserContent<SqlSnippets.Content> => {
return {
...NEW_SQL_SNIPPET_SKELETON,
id,
...(name && { name }),
...(owner_id && { owner_id }),
...(project_id && { project_id }),
content: {
...NEW_SQL_SNIPPET_SKELETON.content,
content_id: '',
content_id: id ?? '',
sql: sql ?? '',
},
}
@@ -4,6 +4,7 @@ import { observer } from 'mobx-react-lite'
import { useParams, useTelemetryProps } from 'common'
import { SQL_TEMPLATES } from 'components/interfaces/SQLEditor/SQLEditor.constants'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { SqlSnippet } from 'data/content/sql-snippets-query'
import { useCheckPermissions, useStore } from 'hooks'
import { uuidv4 } from 'lib/helpers'
@@ -19,6 +20,7 @@ const SQLTemplates = observer(() => {
const { ref } = useParams()
const router = useRouter()
const { profile } = useProfile()
const { project } = useProjectContext()
const [sql, quickStart] = partition(SQL_TEMPLATES, { type: 'template' })
const telemetryProps = useTelemetryProps()
@@ -28,10 +30,9 @@ const SQLTemplates = observer(() => {
subject: { id: profile?.id },
})
// [Joshen TODO] Removed optimistic query creation logic for now, need to figure out
// how to do that after using ids as part of the URL
const handleNewQuery = async (sql: string, name: string) => {
if (!ref) return console.error('Project ref is required')
if (!project) return console.error('Project is required')
if (!profile) return console.error('Profile is required')
if (!canCreateSQLSnippet) {
return ui.setNotification({
category: 'info',
@@ -40,10 +41,16 @@ const SQLTemplates = observer(() => {
}
try {
const snippet = createSqlSnippetSkeleton({ name, sql, owner_id: profile?.id })
const data = { ...snippet, id: uuidv4() }
snap.addSnippet(data as SqlSnippet, ref)
router.push(`/project/${ref}/sql/${data.id}`)
const snippet = createSqlSnippetSkeleton({
id: uuidv4(),
name,
sql,
owner_id: profile.id,
project_id: project.id,
})
snap.addSnippet(snippet as SqlSnippet, project.ref)
router.push(`/project/${project.ref}/sql/${snippet.id}`)
} catch (error: any) {
ui.setNotification({
category: 'error',
@@ -16,6 +16,7 @@ import { useProfile } from 'lib/profile'
import { useSnippets, useSqlEditorStateSnapshot } from 'state/sql-editor'
import { Button, Dropdown, IconPlus, IconSearch, IconX, Input, Menu, useCommandMenu } from 'ui'
import { COMMAND_ROUTES } from 'ui/src/components/Command/Command.constants'
import { useProjectContext } from '../ProjectLayout/ProjectContext'
import QueryItem from './QueryItem'
const SideBarContent = observer(() => {
@@ -26,6 +27,7 @@ const SideBarContent = observer(() => {
const [filterString, setFilterString] = useState('')
const { setPages, setIsOpen } = useCommandMenu()
const showCmdkHelper = useFlag('dashboardCmdk')
const { project } = useProjectContext()
const snap = useSqlEditorStateSnapshot()
const { isLoading, isSuccess } = useSqlSnippetsQuery(ref, {
@@ -56,7 +58,8 @@ const SideBarContent = observer(() => {
})
const handleNewQuery = async () => {
if (!ref) return console.error('Project ref is required')
if (!project) return console.error('Project is required')
if (!profile) return console.error('Profile is required')
if (!canCreateSQLSnippet) {
return ui.setNotification({
category: 'info',
@@ -65,12 +68,16 @@ const SideBarContent = observer(() => {
}
try {
const snippet = createSqlSnippetSkeleton({ name: 'Untitled query', owner_id: profile?.id })
const data = { ...snippet, id: uuidv4() }
const snippet = createSqlSnippetSkeleton({
id: uuidv4(),
name: 'Untitled query',
owner_id: profile.id,
project_id: project.id,
})
snap.addSnippet(data as SqlSnippet, ref)
snap.addSnippet(snippet as SqlSnippet, project.ref)
router.push(`/project/${ref}/sql/${data.id}`)
router.push(`/project/${project.ref}/sql/${snippet.id}`)
} catch (error: any) {
ui.setNotification({
category: 'error',
+327 -10
View File
@@ -396,6 +396,8 @@ export interface paths {
"/platform/projects/{ref}/content": {
/** Gets project's content */
get: operations["ContentController_getContent"];
/** Updates project's content */
put: operations["ContentController_updateWholeContent"];
/** Creates project's content */
post: operations["ContentController_createContent"];
/** Deletes project's content */
@@ -688,9 +690,13 @@ export interface paths {
*/
post: operations["VercelIntegrationController_createVercelIntegration"];
};
"/platform/integrations/vercel/{organization_integration_id}": {
/** Removes Vercel organization integration with the given id */
delete: operations["VercelIntegrationController_removeVercelIntegration"];
};
"/platform/integrations/vercel/projects/{organization_integration_id}": {
/** Gets vercel projects for the given organization */
get: operations["VercelProjectController_getProjects"];
/** Gets vercel projects with the given organization integration id */
get: operations["VercelProjectController_getVercelProjects"];
};
"/platform/integrations/vercel/connections/{organization_integration_id}": {
/** Gets installed vercel project connections for the given organization integration */
@@ -700,6 +706,10 @@ export interface paths {
/** Connects a Vercel project to a supabase project */
post: operations["VercelConnectionsController_createVercelConnection"];
};
"/platform/integrations/vercel/connections/{connection_id}/sync-envs": {
/** Syncs supabase project envs with given connection id */
post: operations["VercelConnectionsController_syncVercelConnectionEnvs"];
};
"/platform/integrations/vercel/connections/{connection_id}": {
/** Deletes vercel project connection */
delete: operations["VercelConnectionsController_deleteVercelConnection"];
@@ -1044,6 +1054,8 @@ export interface paths {
"/v0/projects/{ref}/content": {
/** Gets project's content */
get: operations["ContentController_getContent"];
/** Updates project's content */
put: operations["ContentController_updateWholeContent"];
/** Creates project's content */
post: operations["ContentController_createContent"];
/** Deletes project's content */
@@ -1235,6 +1247,23 @@ export interface paths {
/** Deletes objects */
delete: operations["StorageObjectsController_deleteObjects"];
};
"/v1/branch/{branch_id}": {
/**
* Get database branch config
* @description Fetches configurations of the specified database branch
*/
get: operations["BranchController_getBranchDetails"];
/**
* Delete a database branch
* @description Deletes the specified database branch
*/
delete: operations["BranchController_deleteBranch"];
/**
* Update database branch config
* @description Updates the configuration of the specified database branch
*/
patch: operations["BranchController_updateBranch"];
};
"/v1/projects": {
/**
* List all projects
@@ -1247,6 +1276,23 @@ export interface paths {
"/v1/projects/{ref}/api-keys": {
get: operations["ApiKeysController_getProjectApiKeys"];
};
"/v1/projects/{ref}/branches": {
/**
* List all database branches
* @description Returns all database branches of the specified project.
*/
get: operations["BranchesController_getBranches"];
/**
* Create a database branch
* @description Creates a database branch from the specified project.
*/
post: operations["BranchesController_createBranch"];
/**
* Disables preview branching
* @description Disables preview branching for the specified project
*/
delete: operations["BranchesController_disableBranch"];
};
"/v1/projects/{ref}/custom-hostname": {
/** Gets project's custom hostname config */
get: operations["CustomHostnamesController_getCustomHostnameConfig"];
@@ -2992,6 +3038,29 @@ export interface components {
anon_key: string;
service_key: string;
};
GetUserContentObject: {
owner: {
id?: number;
username?: string;
};
updated_by: {
id?: number;
username?: string;
};
id: string;
inserted_at: string;
updated_at: string;
type: Record<string, never>;
visibility: Record<string, never>;
name: string;
description?: string;
project_id: number;
owner_id: number;
last_updated_by: number;
};
GetUserContentResponse: {
data: (components["schemas"]["GetUserContentObject"])[];
};
CreateContentParams: {
id: string;
name: string;
@@ -3003,6 +3072,30 @@ export interface components {
content?: Record<string, never>;
owner_id?: number;
};
UserContentObject: {
id: string;
inserted_at: string;
updated_at: string;
type: Record<string, never>;
visibility: Record<string, never>;
name: string;
description?: string;
project_id: number;
owner_id: number;
last_updated_by: number;
};
UpsertContentParams: {
id: string;
name: string;
description: string;
/** @enum {string} */
type: "sql" | "report" | "log_sql";
/** @enum {string} */
visibility: "user" | "project" | "org" | "public";
content?: Record<string, never>;
owner_id?: number;
project_id: number;
};
UpdateContentParams: {
id?: string;
name?: string;
@@ -3026,6 +3119,15 @@ export interface components {
postgrest: string;
"supabase-postgres": string;
};
BranchResponse: {
id: string;
name: string;
project_ref: string;
is_default: boolean;
git_branch: string;
created_at: string;
updated_at: string;
};
ProjectDetailResponse: {
cloud_provider: string;
db_host: string;
@@ -3046,6 +3148,7 @@ export interface components {
volumeSizeGb?: number;
maxDatabasePreprovisionGb?: number;
lastDatabaseResizeAt?: string;
preview_branches: (components["schemas"]["BranchResponse"])[];
};
ProjectRefResponse: {
id: number;
@@ -3704,6 +3807,14 @@ export interface components {
framework?: string | null;
link?: components["schemas"]["VercelProjectLink"];
};
GetVercelProjectsResponse: {
projects: (components["schemas"]["IntegrationVercelProject"])[];
pagination: {
count?: number;
next?: number | null;
prev?: number | null;
};
};
GetVercelConnections: {
id: string;
inserted_at: string;
@@ -3798,6 +3909,21 @@ export interface components {
GetMetricsResponse: {
metrics: (components["schemas"]["ProjectMetric"])[];
};
BranchDetailResponse: {
db_port: number;
ref: string;
postgres_version: string;
/** @enum {string} */
status: "ACTIVE_HEALTHY" | "ACTIVE_UNHEALTHY" | "COMING_UP" | "GOING_DOWN" | "INACTIVE" | "INIT_FAILED" | "REMOVED" | "RESTORING" | "UNKNOWN" | "UPGRADING" | "PAUSING";
db_host: string;
db_user?: string;
db_pass?: string;
jwt_secret?: string;
};
UpdateBranchBody: {
branch_name?: string;
git_branch?: string;
};
DatabaseResponse: {
/** @description Database host */
host: string;
@@ -3808,6 +3934,11 @@ export interface components {
name: string;
api_key: string;
};
CreateBranchBody: {
branch_name: string;
git_branch?: string;
region?: string;
};
UpdateCustomHostnameResponse: {
/** @enum {string} */
status: "1_not_started" | "2_initiated" | "3_challenge_verified" | "4_origin_setup_completed" | "5_services_reconfigured";
@@ -6639,11 +6770,38 @@ export interface operations {
};
};
responses: {
200: never;
200: {
content: {
"application/json": components["schemas"]["GetUserContentResponse"];
};
};
/** @description Failed to retrieve project's content */
500: never;
};
};
/** Updates project's content */
ContentController_updateWholeContent: {
parameters: {
path: {
/** @description Project ref */
ref: string;
};
};
requestBody: {
content: {
"application/json": components["schemas"]["UpsertContentParams"];
};
};
responses: {
200: {
content: {
"application/json": components["schemas"]["UserContentObject"];
};
};
/** @description Failed to update project's content */
500: never;
};
};
/** Creates project's content */
ContentController_createContent: {
parameters: {
@@ -6660,7 +6818,7 @@ export interface operations {
responses: {
201: {
content: {
"application/json": (Record<string, never>)[];
"application/json": (components["schemas"]["UserContentObject"])[];
};
};
/** @description Failed to create project's content */
@@ -6677,7 +6835,7 @@ export interface operations {
responses: {
200: {
content: {
"application/json": Record<string, never>;
"application/json": components["schemas"]["UserContentObject"];
};
};
/** @description Failed to delete project's content */
@@ -6699,7 +6857,7 @@ export interface operations {
responses: {
200: {
content: {
"application/json": (Record<string, never>)[];
"application/json": (components["schemas"]["UserContentObject"])[];
};
};
/** @description Failed to update project's content */
@@ -8072,20 +8230,38 @@ export interface operations {
500: never;
};
};
/** Gets vercel projects for the given organization */
VercelProjectController_getProjects: {
/** Removes Vercel organization integration with the given id */
VercelIntegrationController_removeVercelIntegration: {
parameters: {
path: {
organization_integration_id: string;
};
};
responses: {
200: never;
/** @description Failed to remove Vercel organization integration with the given id */
500: never;
};
};
/** Gets vercel projects with the given organization integration id */
VercelProjectController_getVercelProjects: {
parameters: {
query: {
search?: string;
from?: string;
limit: string;
};
path: {
organization_integration_id: string;
};
};
responses: {
200: {
content: {
"application/json": (components["schemas"]["IntegrationVercelProject"])[];
"application/json": components["schemas"]["GetVercelProjectsResponse"];
};
};
/** @description Failed to get vercel projects for the given organization */
/** @description Failed to get vercel projects with the given organization integration id */
500: never;
};
};
@@ -8119,6 +8295,19 @@ export interface operations {
500: never;
};
};
/** Syncs supabase project envs with given connection id */
VercelConnectionsController_syncVercelConnectionEnvs: {
parameters: {
path: {
connection_id: string;
};
};
responses: {
201: never;
/** @description Failed to sync supabase project envs with given connection id */
500: never;
};
};
/** Deletes vercel project connection */
VercelConnectionsController_deleteVercelConnection: {
parameters: {
@@ -8517,6 +8706,70 @@ export interface operations {
};
};
};
/**
* Get database branch config
* @description Fetches configurations of the specified database branch
*/
BranchController_getBranchDetails: {
parameters: {
path: {
/** @description Branch ID */
branch_id: string;
};
};
responses: {
200: {
content: {
"application/json": components["schemas"]["BranchDetailResponse"];
};
};
/** @description Failed to update database branch */
500: never;
};
};
/**
* Delete a database branch
* @description Deletes the specified database branch
*/
BranchController_deleteBranch: {
parameters: {
path: {
/** @description Branch ID */
branch_id: string;
};
};
responses: {
200: never;
/** @description Failed to delete database branch */
500: never;
};
};
/**
* Update database branch config
* @description Updates the configuration of the specified database branch
*/
BranchController_updateBranch: {
parameters: {
path: {
/** @description Branch ID */
branch_id: string;
};
};
requestBody: {
content: {
"application/json": components["schemas"]["UpdateBranchBody"];
};
};
responses: {
200: {
content: {
"application/json": components["schemas"]["BranchResponse"];
};
};
/** @description Failed to update database branch */
500: never;
};
};
ApiKeysController_getProjectApiKeys: {
parameters: {
path: {
@@ -8533,6 +8786,70 @@ export interface operations {
403: never;
};
};
/**
* List all database branches
* @description Returns all database branches of the specified project.
*/
BranchesController_getBranches: {
parameters: {
path: {
/** @description Project ref */
ref: string;
};
};
responses: {
200: {
content: {
"application/json": (components["schemas"]["BranchResponse"])[];
};
};
/** @description Failed to retrieve database branches */
500: never;
};
};
/**
* Create a database branch
* @description Creates a database branch from the specified project.
*/
BranchesController_createBranch: {
parameters: {
path: {
/** @description Project ref */
ref: string;
};
};
requestBody: {
content: {
"application/json": components["schemas"]["CreateBranchBody"];
};
};
responses: {
201: {
content: {
"application/json": components["schemas"]["BranchResponse"];
};
};
/** @description Failed to create database branch */
500: never;
};
};
/**
* Disables preview branching
* @description Disables preview branching for the specified project
*/
BranchesController_disableBranch: {
parameters: {
path: {
/** @description Project ref */
ref: string;
};
};
responses: {
200: never;
/** @description Failed to disable preview branching */
500: never;
};
};
/** Gets project's custom hostname config */
CustomHostnamesController_getCustomHostnameConfig: {
parameters: {
+26 -11
View File
@@ -1,28 +1,43 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import { put } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { UserContent } from 'types'
import { contentKeys } from './keys'
type UpsertContentVariables = {
import { components } from 'data/api'
import { put } from 'data/fetchers'
import { contentKeys } from './keys'
import { Content } from './content-query'
export type UpsertContentPayload = Omit<components['schemas']['UpsertContentParams'], 'content'> & {
content: Content['content']
}
export type UpsertContentVariables = {
projectRef: string
id: string
payload: Partial<UserContent>
payload: UpsertContentPayload
}
export async function upsertContent(
{ projectRef, payload }: UpsertContentVariables,
signal?: AbortSignal
) {
const response = await put<UserContent[]>(`${API_URL}/projects/${projectRef}/content`, payload, {
const { data, error } = await put('/platform/projects/{ref}/content', {
params: { path: { ref: projectRef } },
body: {
id: payload.id,
name: payload.name,
description: payload.description,
project_id: payload.project_id,
owner_id: payload.owner_id,
type: payload.type,
visibility: payload.visibility,
content: payload.content as any,
},
signal,
})
if (error) throw error
if (response.error) throw response.error
return { content: response }
return data
}
type UpsertContentData = Awaited<ReturnType<typeof upsertContent>>
export type UpsertContentData = Awaited<ReturnType<typeof upsertContent>>
export const useContentUpsertMutation = ({
onSuccess,
+15 -10
View File
@@ -1,8 +1,8 @@
import { Content } from 'data/content/content-query'
import { upsertContent } from 'data/content/content-upsert-mutation'
import { UpsertContentPayload, upsertContent } from 'data/content/content-upsert-mutation'
import { SqlSnippet } from 'data/content/sql-snippets-query'
import { debounce, memoize } from 'lodash'
import { useMemo } from 'react'
import { SqlSnippets } from 'types'
import { proxy, snapshot, subscribe, useSnapshot } from 'valtio'
import { devtools, proxySet } from 'valtio/utils'
@@ -170,10 +170,13 @@ export const useSnippets = (projectRef: string | undefined) => {
}, [projectRef, snapshot.orders, snapshot.snippets])
}
async function upsert(id: string, projectRef: string, content: Partial<Content>) {
async function upsert(id: string, projectRef: string, payload: UpsertContentPayload) {
try {
sqlEditorState.savingStates[id] = 'UPDATING'
await upsertContent({ projectRef, id, payload: content })
await upsertContent({
projectRef,
payload,
})
sqlEditorState.savingStates[id] = 'IDLE'
} catch (error) {
sqlEditorState.savingStates[id] = 'UPDATING_FAILED'
@@ -181,8 +184,8 @@ async function upsert(id: string, projectRef: string, content: Partial<Content>)
}
const memoizedUpdate = memoize((_id: string) => debounce(upsert, 1000))
const debouncedUpdate = (id: string, projectRef: string, content: Partial<Content>) =>
memoizedUpdate(id)(id, projectRef, content)
const debouncedUpdate = (id: string, projectRef: string, payload: UpsertContentPayload) =>
memoizedUpdate(id)(id, projectRef, payload)
if (typeof window !== 'undefined') {
devtools(sqlEditorState, { name: 'sqlEditorState', enabled: true })
@@ -195,12 +198,14 @@ if (typeof window !== 'undefined') {
if (snippet) {
debouncedUpdate(id, snippet.projectRef, {
content: { ...snippet.snippet.content, content_id: id },
...snippet.snippet,
name: snippet.snippet.name ?? 'Untitled',
description: snippet.snippet.description ?? '',
visibility: 'user',
project_id: snippet.snippet.project_id ?? 0,
content: { ...snippet.snippet.content, content_id: id } as SqlSnippets.Content,
type: 'sql',
id,
name: snippet.snippet.name,
description: snippet.snippet.description,
visibility: snippet.snippet.visibility ?? 'project',
})
sqlEditorState.needsSaving.delete(id)