Files
Joshen Lim 17e0c5dbcf Improve snippet organization in SQL Editor (#27881)
* Init changes for sql fodlers

* Added upsert logic in sql-editor-v2 valtio, hooked up with templates and quickstarts

* Do up logic for creating new snippets by typing in /new or by clicking new query button

* Do up logic for updating and deleting snippets

* Do up logic for favourites and shared snippets

* Do up logic for favourites and shared snippets

* Fix

* Fix saving indicator, add empty states for favorites and shared snippets

* Implement sorting

* Some minor QOL improvements

* Minor fix on empty state for private snippets

* Add delete folder mutation

* Implement create and update folder

* Fix reinstate with AI renaming for new snippets under folder

* Support controlled multi select behaviour in private snippets

* Undo changes to tree-view-multi-select

* Support bulk deletes

* Support moving queries + rendering queries in folders

* Support deleting folders and creating a new folder when moving a query

* Fix bug where renaming query removes content

* Add initial loading state in sql editor nav + handle fallback if cannot retrieve content by id

* Fix some spelling

* Fix TS issue in sql folders mutation keys

* Fix toggling favorite

* Lint

* Revert fallback behaviour in ]id] for now

* Fix favorites and shared snippets not showing

* Fix moving currently opened snippet leads to loading

* Support bulk moving

* Improve multi select logic a little

* Nit lint

* Reinstate AI retitling for untitled snippets when running query

* Remove hardcode in useAFlag

* Support creating new snippet in a folder directly

* Fix sharing snippets that are within a folder

* Fix sharing snippets within a folder

* Fix favorite

* Add loading state when fetching folder contents

* Fix favoriting snippets in folders
2024-07-17 12:23:19 +08:00

191 lines
5.4 KiB
TypeScript

import { SnippetDetail } from 'data/content/sql-folders-query'
import { removeCommentsFromSql } from 'lib/helpers'
import type { SqlSnippets, UserContent } from 'types'
import {
NEW_SQL_SNIPPET_SKELETON,
destructiveSqlRegex,
sqlAiDisclaimerComment,
} from './SQLEditor.constants'
import { ContentDiff, DiffType } from './SQLEditor.types'
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: id ?? '',
sql: sql ?? '',
},
}
}
export const createSqlSnippetSkeletonV2 = ({
id,
name,
sql,
owner_id,
project_id,
folder_id,
}: {
id: string
name: string
sql: string
owner_id: number
project_id: number
folder_id?: string
}): SnippetDetail => {
return {
...NEW_SQL_SNIPPET_SKELETON,
id,
owner_id,
project_id,
name,
folder_id,
favorite: false,
inserted_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
content: {
...NEW_SQL_SNIPPET_SKELETON.content,
content_id: id ?? '',
sql: sql ?? '',
} as any,
}
}
export function getDiffTypeButtonLabel(diffType: DiffType) {
switch (diffType) {
case DiffType.Modification:
return 'Accept change'
case DiffType.Addition:
return 'Accept addition'
case DiffType.NewSnippet:
return 'Create new snippet'
default:
throw new Error(`Unknown diff type '${diffType}'`)
}
}
export function getDiffTypeDropdownLabel(diffType: DiffType) {
switch (diffType) {
case DiffType.Modification:
return 'Compare as change'
case DiffType.Addition:
return 'Compare as addition'
case DiffType.NewSnippet:
return 'Compare as new snippet'
default:
throw new Error(`Unknown diff type '${diffType}'`)
}
}
export function checkDestructiveQuery(sql: string) {
return destructiveSqlRegex.some((regex) => regex.test(removeCommentsFromSql(sql)))
}
export const generateMigrationCliCommand = (id: string, name: string, isNpx = false) =>
`
${isNpx ? 'npx ' : ''}supabase snippets download ${id} |
${isNpx ? 'npx ' : ''}supabase migration new ${name}
`.trim()
export const generateSeedCliCommand = (id: string, isNpx = false) =>
`
${isNpx ? 'npx ' : ''}supabase snippets download ${id} >> \\
supabase/seed.sql
`.trim()
export const generateFileCliCommand = (id: string, name: string, isNpx = false) =>
`
${isNpx ? 'npx ' : ''}supabase snippets download ${id} > \\
${name}.sql
`.trim()
export const compareAsModification = (sqlDiff: ContentDiff) => {
const formattedModified = sqlDiff.modified.replace(sqlAiDisclaimerComment, '').trim()
return {
original: sqlDiff.original,
modified: `${sqlAiDisclaimerComment}\n\n${formattedModified}`,
}
}
export const compareAsAddition = (sqlDiff: ContentDiff) => {
const formattedOriginal = sqlDiff.original.replace(sqlAiDisclaimerComment, '').trim()
const formattedModified = sqlDiff.modified.replace(sqlAiDisclaimerComment, '').trim()
const newModified =
sqlAiDisclaimerComment +
'\n\n' +
(formattedOriginal ? formattedOriginal + '\n\n' : '') +
formattedModified
return {
original: sqlDiff.original,
modified: newModified,
}
}
export const compareAsNewSnippet = (sqlDiff: ContentDiff) => {
return {
original: '',
modified: sqlDiff.modified,
}
}
// [Joshen] Just FYI as well the checks here on whether to append limit is quite restricted
// This is to prevent dashboard from accidentally appending limit to the end of a query
// thats not supposed to have any, since there's too many cases to cover.
// We can however look into making this logic better in the future
// i.e It's harder to append the limit param, than just leaving the query as it is
// Otherwise we'd need a full on parser to do this properly
export const checkIfAppendLimitRequired = (sql: string, limit: number = 0) => {
// Remove lines and whitespaces to use for checking
const cleanedSql = sql.trim().replaceAll('\n', ' ').replaceAll(/\s+/g, ' ')
// Check how many queries
const regMatch = cleanedSql.matchAll(/[a-zA-Z]*[0-9]*[;]+/g)
const queries = new Array(...regMatch)
const indexSemiColon = cleanedSql.lastIndexOf(';')
const hasComments = cleanedSql.includes('--')
const hasMultipleQueries =
queries.length > 1 || (indexSemiColon > 0 && indexSemiColon !== cleanedSql.length - 1)
// Check if need to auto limit rows
const appendAutoLimit =
limit > 0 &&
!hasComments &&
!hasMultipleQueries &&
cleanedSql.toLowerCase().startsWith('select') &&
!cleanedSql.toLowerCase().match(/fetch\s+first/i) &&
!cleanedSql.match(/limit$/i) &&
!cleanedSql.match(/limit;$/i) &&
!cleanedSql.match(/limit [0-9]* offset [0-9]*[;]?$/i) &&
!cleanedSql.match(/limit [0-9]*[;]?$/i)
return { cleanedSql, appendAutoLimit }
}
export const suffixWithLimit = (sql: string, limit: number = 0) => {
const { cleanedSql, appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
const formattedSql = appendAutoLimit
? cleanedSql.endsWith(';')
? sql.replace(/[;]+$/, ` limit ${limit};`)
: `${sql} limit ${limit};`
: sql
return formattedSql
}