Skip to content

Commit

Permalink
dedupe
Browse files Browse the repository at this point in the history
  • Loading branch information
mmkal committed May 21, 2024
1 parent dd2f8bc commit d91c1ff
Show file tree
Hide file tree
Showing 5 changed files with 49 additions and 48 deletions.
17 changes: 12 additions & 5 deletions packages/admin/src/client/utils/MutationButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {ComponentProps} from 'react'
import {ComponentProps} from 'react'
import {toast} from 'sonner'
import {RQMutationLike, useConfirmable} from './destructive'
import {trpc} from './trpc'
Expand All @@ -13,11 +13,15 @@ export interface TRPCMutationProcedureHelper<Options, Mutation> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type BaseOptions = {onSuccess?: (data: any) => unknown}

export type Awaitable<T> = T | Promise<T>

interface MutationButtonProps<Options, Mutation> extends ComponentProps<typeof Button> {
mutation: TRPCMutationProcedureHelper<Options, Mutation>
icon?: keyof typeof icons
options?: Options
args?: Mutation extends RQMutationLike<infer Variables> ? Variables | null | (() => Variables | null) : never
args?: Mutation extends RQMutationLike<infer Variables>
? Variables | null | (() => Awaitable<Variables | null>)
: never
}

export function MutationButton<Options, Mutation>({
Expand All @@ -28,6 +32,7 @@ export function MutationButton<Options, Mutation>({
children,
...buttonProps
}: MutationButtonProps<Options, Mutation>): JSX.Element {
'' as Exclude<keyof typeof buttonProps, keyof ComponentProps<typeof Button>> satisfies never // make sure we aren't spreading custom props into Button
const options = _options as Record<string, Function>
const util = trpc.useUtils()
const mutation = useConfirmable(
Expand All @@ -47,11 +52,11 @@ export function MutationButton<Options, Mutation>({
return (
<Button
{...buttonProps}
onClick={() => {
onClick={async () => {
let args: [] | undefined
if (typeof _args === 'function') {
try {
args = _args() as []
args = (await _args()) as []
} catch (e) {
toast.error(String(e))
}
Expand All @@ -62,7 +67,9 @@ export function MutationButton<Options, Mutation>({
} else if (_args === null) {
args = undefined
} else {
throw new Error(`Invalid args. Expected null, undefined, array or function that returns array. Got ${_args}`)
throw new Error(
`Invalid args. Expected null, undefined, array or function that returns array. Got ${(_args as Object)?.constructor?.name || typeof _args}`,
)
}
if (Array.isArray(args)) {
mutation.mutate(...args)
Expand Down
19 changes: 1 addition & 18 deletions packages/admin/src/client/utils/destructive.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,5 @@
import {useMutation} from '@tanstack/react-query'
import {AlertOptions, useAlerter} from './alerter'

export function useDestructive<T extends {mutateAsync: Function}>(
input: T,
...alerterArgs: [title?: string, options?: AlertOptions]
) {
const alerter = useAlerter()
const wrapped = useMutation(async (...args: never[]) => {
const [title = 'Are you sure?', options = {}] = alerterArgs
const confirmed = await alerter.confirm(title, options)
if (!confirmed) return
return input.mutateAsync(...args) as unknown
}) as T

return Object.assign(wrapped, {
disable: (disable: boolean | undefined) => (disable ? input : wrapped),
})
}
import {useAlerter} from './alerter'

export type RQMutationLike<P extends unknown[]> = {
mutate: (...args: P) => unknown
Expand Down
7 changes: 6 additions & 1 deletion packages/admin/src/client/utils/trpc.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ export function useTrpcClient() {
const message = String(error)
if (message.includes('confirmation_missing:')) return // handled elsewhere

toast.error(message)
const casted = (error || {}) as {toasted?: boolean}
if (!casted?.toasted) {
// workaround: we wrap some mutations in a useConfirmable hook that will cause errors to bubble up twice
toast.error(message)
casted.toasted = true
}
},
}),
queryCache: new QueryCache({
Expand Down
20 changes: 12 additions & 8 deletions packages/admin/src/client/views/Migrations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {useLocalStorage} from 'react-use'
import {toast} from 'sonner'
import {MeasuredCodeMirror} from '../sql-codemirror'
import {MutationButton} from '../utils/MutationButton'
import {useAlerter} from '../utils/alerter'
import {createCascadingState} from '../utils/cascading-state'
import {useConfirmable} from '../utils/destructive'
import {trpc} from '../utils/trpc'
Expand Down Expand Up @@ -112,6 +113,8 @@ function _Migrations() {
const list = trpc.migrations.rawList.useQuery()
const definitions = trpc.migrations.definitions.filepath.useQuery()

const alerter = useAlerter()

const filesData = React.useMemo(() => {
const fsEntries = (list.data || [])
.flatMap(m => {
Expand Down Expand Up @@ -158,8 +161,8 @@ function _Migrations() {
title="Create migration"
icon="SquarePlus"
mutation={trpc.migrations.create}
args={() => {
const name = prompt('name?')
args={async () => {
const name = await alerter.prompt('name?')
if (!name) return null

return [{name}] as const
Expand Down Expand Up @@ -240,7 +243,7 @@ function _Migrations() {
onSuccess: data => {
const message = data.updated
? 'Database state repaired successfully'
: 'Database state was already valid'
: 'Database state already valid'
toast.success(message)
},
}}
Expand Down Expand Up @@ -342,7 +345,7 @@ const _sampleFilesJson: Record<string, string> = {
export const FileTree = (tree: File | Folder) => {
const [fileState, setFileState] = file.useState()

const {up, down, rebase, list, definitions, updateDBFromDefinitions} = useMigrations()
const {list, definitions} = useMigrations()

if (tree.type === 'file') {
const fileInfo = list.data?.find(f => f.path === tree.path)
Expand All @@ -362,10 +365,11 @@ export const FileTree = (tree: File | Folder) => {
{tree.path === definitions.data?.path && (
<ContextMenuContent className="mt-5 bg-gray-800 text-gray-100">
<ContextMenuItem className="p-0">
<Button className="gap-2 flex-1 justify-start" onClick={() => updateDBFromDefinitions.mutate()}>
<icons.Book />
Update database to match this definitions file
</Button>
<MutationButton
className="gap-2 flex-1 justify-start"
icon="Book"
mutation={trpc.migrations.definitions.updateDb}
/>
</ContextMenuItem>
</ContextMenuContent>
)}
Expand Down
34 changes: 18 additions & 16 deletions packages/migrator/src/migrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,32 +395,34 @@ export class Migrator {
nameSuffix = 'update.sql'
}

const template = this.templates[path.extname(nameSuffix)]
if (typeof template !== 'string') {
// eslint-disable-next-line unicorn/prefer-type-error
throw new Error(`Unsupported file extension ${JSON.stringify(path.extname(nameSuffix))}`)
}

const name = this.filePrefix() + nameSuffix
const filepath = path.join(this.migratorOptions.migrationsPath, name)

if (!content) {
content = this.template(filepath)
content = template
}

await fs.writeFile(filepath, content)
return {name, path: filepath, content}
}

template(filepath: string) {
const ext = path.extname(filepath)
const esm = typeof require?.main === 'object'

if (ext === '.js' && esm) return templates.esm
if (ext === '.js') return templates.cjs
if (ext === '.ts') return templates.typescript
if (ext === '.mjs') return templates.esm
if (ext === '.cjs') return templates.cjs

if (['.ts', '.cts', '.mts'].includes(ext)) return templates.typescript

if (ext === '.sql') return templates.sql

throw new Error(`Unsupported file extension ${ext}`)
get templates(): Record<string, string> {
const js = typeof require?.main === 'object' ? templates.cjs : templates.esm
return {
'.js': js,
'.ts': templates.typescript,
'.cts': templates.typescript,
'.mts': templates.typescript,
'.mjs': templates.esm,
'.cjs': templates.cjs,
'.sql': templates.sql,
}
}

/**
Expand Down

0 comments on commit d91c1ff

Please sign in to comment.