Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
B
bit-pm
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
燕伟桐
bit-pm
Commits
382fe9ba
Unverified
提交
382fe9ba
authored
6月 17, 2025
作者:
Will Chen
提交者:
GitHub
6月 17, 2025
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
support setting for writing supabase migration files (#427)
上级
ff4e93d7
隐藏空白字符变更
内嵌
并排
正在显示
10 个修改的文件
包含
206 行增加
和
20 行删除
+206
-20
execute-sql-1.md
e2e-tests/fixtures/execute-sql-1.md
+7
-0
execute-sql-no-description.md
e2e-tests/fixtures/execute-sql-no-description.md
+7
-0
test_helper.ts
e2e-tests/helpers/test_helper.ts
+4
-0
supabase_migrations.spec.ts
e2e-tests/supabase_migrations.spec.ts
+61
-0
SupabaseIntegration.tsx
src/components/SupabaseIntegration.tsx
+58
-19
response_processor.ts
src/ipc/processors/response_processor.ts
+20
-1
file_utils.ts
src/ipc/utils/file_utils.ts
+35
-0
schemas.ts
src/lib/schemas.ts
+1
-0
supabase_context.ts
src/supabase_admin/supabase_context.ts
+9
-0
supabase_management_client.ts
src/supabase_admin/supabase_management_client.ts
+4
-0
没有找到文件。
e2e-tests/fixtures/execute-sql-1.md
0 → 100644
浏览文件 @
382fe9ba
Example SQL
<dyad-execute-sql
description=
"create_users_table"
>
CREATE TABLE users (id serial primary key);
</dyad-execute-sql>
Done.
e2e-tests/fixtures/execute-sql-no-description.md
0 → 100644
浏览文件 @
382fe9ba
No description!
<dyad-execute-sql>
DROP TABLE users;
</dyad-execute-sql>
Done.
e2e-tests/helpers/test_helper.ts
浏览文件 @
382fe9ba
...
@@ -579,6 +579,10 @@ export class PageObject {
...
@@ -579,6 +579,10 @@ export class PageObject {
await
this
.
page
.
getByRole
(
"link"
,
{
name
:
"Apps"
}).
click
();
await
this
.
page
.
getByRole
(
"link"
,
{
name
:
"Apps"
}).
click
();
}
}
async
goToChatTab
()
{
await
this
.
page
.
getByRole
(
"link"
,
{
name
:
"Chat"
}).
click
();
}
async
goToHubTab
()
{
async
goToHubTab
()
{
await
this
.
page
.
getByRole
(
"link"
,
{
name
:
"Hub"
}).
click
();
await
this
.
page
.
getByRole
(
"link"
,
{
name
:
"Hub"
}).
click
();
}
}
...
...
e2e-tests/supabase_migrations.spec.ts
0 → 100644
浏览文件 @
382fe9ba
import
{
expect
}
from
"@playwright/test"
;
import
{
test
}
from
"./helpers/test_helper"
;
import
fs
from
"fs-extra"
;
import
path
from
"path"
;
test
(
"supabase migrations"
,
async
({
po
})
=>
{
await
po
.
setUp
({
autoApprove
:
true
});
await
po
.
sendPrompt
(
"tc=add-supabase"
);
// Connect to Supabase
await
po
.
page
.
getByText
(
"Set up supabase"
).
click
();
await
po
.
clickConnectSupabaseButton
();
await
po
.
clickBackButton
();
const
appPath
=
await
po
.
getCurrentAppPath
();
const
migrationsDir
=
path
.
join
(
appPath
,
"supabase"
,
"migrations"
);
// --- SCENARIO 1: OFF BY DEFAULT ---
await
po
.
sendPrompt
(
"tc=execute-sql-1"
);
await
po
.
waitForChatCompletion
();
expect
(
fs
.
existsSync
(
migrationsDir
)).
toBe
(
false
);
// --- SCENARIO 2: TOGGLE ON ---
// Go to settings to find the Supabase integration
await
po
.
goToSettingsTab
();
const
migrationsSwitch
=
po
.
page
.
locator
(
"#supabase-migrations"
);
await
migrationsSwitch
.
click
();
await
po
.
goToChatTab
();
// Send a prompt that triggers a migration
await
po
.
sendPrompt
(
"tc=execute-sql-1"
);
await
po
.
waitForChatCompletion
();
let
files
:
string
[]
=
[];
await
expect
(
async
()
=>
{
// Check that one migration file was created
files
=
await
fs
.
readdir
(
migrationsDir
);
expect
(
files
).
toHaveLength
(
1
);
}).
toPass
();
expect
(
files
[
0
]).
toMatch
(
/0000_create_users_table
\.
sql/
);
expect
(
await
fs
.
readFile
(
path
.
join
(
migrationsDir
,
files
[
0
]),
"utf8"
)).
toEqual
(
"CREATE TABLE users (id serial primary key);"
,
);
// Send a prompt that triggers a migration
await
po
.
sendPrompt
(
"tc=execute-sql-no-description"
);
await
po
.
waitForChatCompletion
();
await
expect
(
async
()
=>
{
// Check that one migration file was created
files
=
await
fs
.
readdir
(
migrationsDir
);
expect
(
files
).
toHaveLength
(
2
);
}).
toPass
();
expect
(
files
[
1
]).
toMatch
(
/0001_
\w
+_
\w
+_
\w
+
\.
sql/
);
expect
(
await
fs
.
readFile
(
path
.
join
(
migrationsDir
,
files
[
1
]),
"utf8"
)).
toEqual
(
"DROP TABLE users;"
,
);
});
src/components/SupabaseIntegration.tsx
浏览文件 @
382fe9ba
import
{
useState
}
from
"react"
;
import
{
useState
}
from
"react"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Switch
}
from
"@/components/ui/switch"
;
import
{
Label
}
from
"@/components/ui/label"
;
// We might need a Supabase icon here, but for now, let's use a generic one or text.
// We might need a Supabase icon here, but for now, let's use a generic one or text.
// import { Supabase } from "lucide-react"; // Placeholder
// import { Supabase } from "lucide-react"; // Placeholder
import
{
DatabaseZap
}
from
"lucide-react"
;
// Using DatabaseZap as a placeholder
import
{
DatabaseZap
}
from
"lucide-react"
;
// Using DatabaseZap as a placeholder
...
@@ -16,6 +18,8 @@ export function SupabaseIntegration() {
...
@@ -16,6 +18,8 @@ export function SupabaseIntegration() {
// Clear the entire supabase object in settings
// Clear the entire supabase object in settings
const
result
=
await
updateSettings
({
const
result
=
await
updateSettings
({
supabase
:
undefined
,
supabase
:
undefined
,
// Also disable the migration setting on disconnect
enableSupabaseWriteSqlMigration
:
false
,
});
});
if
(
result
)
{
if
(
result
)
{
showSuccess
(
"Successfully disconnected from Supabase"
);
showSuccess
(
"Successfully disconnected from Supabase"
);
...
@@ -31,6 +35,17 @@ export function SupabaseIntegration() {
...
@@ -31,6 +35,17 @@ export function SupabaseIntegration() {
}
}
};
};
const
handleMigrationSettingChange
=
async
(
enabled
:
boolean
)
=>
{
try
{
await
updateSettings
({
enableSupabaseWriteSqlMigration
:
enabled
,
});
showSuccess
(
"Setting updated"
);
}
catch
(
err
:
any
)
{
showError
(
err
.
message
||
"Failed to update setting"
);
}
};
// Check if there's any Supabase accessToken to determine connection status
// Check if there's any Supabase accessToken to determine connection status
const
isConnected
=
!!
settings
?.
supabase
?.
accessToken
;
const
isConnected
=
!!
settings
?.
supabase
?.
accessToken
;
...
@@ -39,26 +54,50 @@ export function SupabaseIntegration() {
...
@@ -39,26 +54,50 @@ export function SupabaseIntegration() {
}
}
return
(
return
(
<
div
className=
"flex items-center justify-between"
>
<
div
>
<
div
>
<
div
className=
"flex items-center justify-between"
>
<
h3
className=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
<
div
>
Supabase Integration
<
h3
className=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
</
h3
>
Supabase Integration
<
p
className=
"text-xs text-gray-500 dark:text-gray-400 mt-1"
>
</
h3
>
Your account is connected to Supabase.
<
p
className=
"text-xs text-gray-500 dark:text-gray-400 mt-1"
>
</
p
>
Your account is connected to Supabase.
</
p
>
</
div
>
<
Button
onClick=
{
handleDisconnectFromSupabase
}
variant=
"destructive"
size=
"sm"
disabled=
{
isDisconnecting
}
className=
"flex items-center gap-2"
>
{
isDisconnecting
?
"Disconnecting..."
:
"Disconnect from Supabase"
}
<
DatabaseZap
className=
"h-4 w-4"
/>
</
Button
>
</
div
>
<
div
className=
"mt-4"
>
<
div
className=
"flex items-center space-x-3"
>
<
Switch
id=
"supabase-migrations"
checked=
{
!!
settings
?.
enableSupabaseWriteSqlMigration
}
onCheckedChange=
{
handleMigrationSettingChange
}
/>
<
div
className=
"space-y-1"
>
<
Label
htmlFor=
"supabase-migrations"
className=
"text-sm font-medium"
>
Write SQL migration files
</
Label
>
<
p
className=
"text-xs text-gray-500 dark:text-gray-400"
>
Generate SQL migration files when modifying your Supabase schema.
This helps you track database changes in version control, though
these files aren't used for chat context, which uses the live
schema.
</
p
>
</
div
>
</
div
>
</
div
>
</
div
>
<
Button
onClick=
{
handleDisconnectFromSupabase
}
variant=
"destructive"
size=
"sm"
disabled=
{
isDisconnecting
}
className=
"flex items-center gap-2"
>
{
isDisconnecting
?
"Disconnecting..."
:
"Disconnect from Supabase"
}
<
DatabaseZap
className=
"h-4 w-4"
/>
{
/* Placeholder icon */
}
</
Button
>
</
div
>
</
div
>
);
);
}
}
src/ipc/processors/response_processor.ts
浏览文件 @
382fe9ba
...
@@ -14,8 +14,10 @@ import {
...
@@ -14,8 +14,10 @@ import {
executeSupabaseSql
,
executeSupabaseSql
,
}
from
"../../supabase_admin/supabase_management_client"
;
}
from
"../../supabase_admin/supabase_management_client"
;
import
{
isServerFunction
}
from
"../../supabase_admin/supabase_utils"
;
import
{
isServerFunction
}
from
"../../supabase_admin/supabase_utils"
;
import
{
SqlQuery
}
from
"../../lib/schemas"
;
import
{
SqlQuery
,
UserSettings
}
from
"../../lib/schemas"
;
import
{
gitCommit
}
from
"../utils/git_utils"
;
import
{
gitCommit
}
from
"../utils/git_utils"
;
import
{
readSettings
}
from
"@/main/settings"
;
import
{
writeMigrationFile
}
from
"../utils/file_utils"
;
const
readFile
=
fs
.
promises
.
readFile
;
const
readFile
=
fs
.
promises
.
readFile
;
const
logger
=
log
.
scope
(
"response_processor"
);
const
logger
=
log
.
scope
(
"response_processor"
);
...
@@ -207,6 +209,7 @@ export async function processFullResponseActions(
...
@@ -207,6 +209,7 @@ export async function processFullResponseActions(
return
{};
return
{};
}
}
const
settings
:
UserSettings
=
readSettings
();
const
appPath
=
getDyadAppPath
(
chatWithApp
.
app
.
path
);
const
appPath
=
getDyadAppPath
(
chatWithApp
.
app
.
path
);
const
writtenFiles
:
string
[]
=
[];
const
writtenFiles
:
string
[]
=
[];
const
renamedFiles
:
string
[]
=
[];
const
renamedFiles
:
string
[]
=
[];
...
@@ -247,6 +250,22 @@ export async function processFullResponseActions(
...
@@ -247,6 +250,22 @@ export async function processFullResponseActions(
supabaseProjectId
:
chatWithApp
.
app
.
supabaseProjectId
!
,
supabaseProjectId
:
chatWithApp
.
app
.
supabaseProjectId
!
,
query
:
query
.
content
,
query
:
query
.
content
,
});
});
// Only write migration file if SQL execution succeeded
if
(
settings
.
enableSupabaseWriteSqlMigration
)
{
try
{
await
writeMigrationFile
(
appPath
,
query
.
content
,
query
.
description
,
);
}
catch
(
error
)
{
errors
.
push
({
message
:
`Failed to write SQL migration file for:
${
query
.
description
}
`
,
error
:
error
,
});
}
}
}
catch
(
error
)
{
}
catch
(
error
)
{
errors
.
push
({
errors
.
push
({
message
:
`Failed to execute SQL query:
${
query
.
content
}
`
,
message
:
`Failed to execute SQL query:
${
query
.
content
}
`
,
...
...
src/ipc/utils/file_utils.ts
浏览文件 @
382fe9ba
import
fs
from
"node:fs"
;
import
fs
from
"node:fs"
;
import
{
promises
as
fsPromises
}
from
"node:fs"
;
import
{
promises
as
fsPromises
}
from
"node:fs"
;
import
path
from
"node:path"
;
import
path
from
"node:path"
;
import
fsExtra
from
"fs-extra"
;
import
{
generateCuteAppName
}
from
"../../lib/utils"
;
/**
/**
* Recursively gets all files in a directory, excluding node_modules and .git
* Recursively gets all files in a directory, excluding node_modules and .git
...
@@ -57,3 +59,36 @@ export async function copyDirectoryRecursive(
...
@@ -57,3 +59,36 @@ export async function copyDirectoryRecursive(
}
}
}
}
}
}
export
async
function
writeMigrationFile
(
appPath
:
string
,
queryContent
:
string
,
queryDescription
?:
string
,
)
{
const
migrationsDir
=
path
.
join
(
appPath
,
"supabase"
,
"migrations"
);
await
fsExtra
.
ensureDir
(
migrationsDir
);
const
files
=
await
fsExtra
.
readdir
(
migrationsDir
);
const
migrationNumbers
=
files
.
map
((
file
)
=>
{
const
match
=
file
.
match
(
/^
(\d{4})
_/
);
return
match
?
parseInt
(
match
[
1
],
10
)
:
-
1
;
})
.
filter
((
num
)
=>
num
!==
-
1
);
const
nextMigrationNumber
=
migrationNumbers
.
length
>
0
?
Math
.
max
(...
migrationNumbers
)
+
1
:
0
;
const
paddedNumber
=
String
(
nextMigrationNumber
).
padStart
(
4
,
"0"
);
let
description
=
"migration"
;
if
(
queryDescription
)
{
description
=
queryDescription
.
toLowerCase
().
replace
(
/
[\s\W
-
]
+/g
,
"_"
);
}
else
{
description
=
generateCuteAppName
().
replace
(
/-/g
,
"_"
);
}
const
migrationFileName
=
`
${
paddedNumber
}
_
${
description
}
.sql`
;
const
migrationFilePath
=
path
.
join
(
migrationsDir
,
migrationFileName
);
await
fsExtra
.
writeFile
(
migrationFilePath
,
queryContent
);
}
src/lib/schemas.ts
浏览文件 @
382fe9ba
...
@@ -142,6 +142,7 @@ export const UserSettingsSchema = z.object({
...
@@ -142,6 +142,7 @@ export const UserSettingsSchema = z.object({
enableProLazyEditsMode
:
z
.
boolean
().
optional
(),
enableProLazyEditsMode
:
z
.
boolean
().
optional
(),
enableProSmartFilesContextMode
:
z
.
boolean
().
optional
(),
enableProSmartFilesContextMode
:
z
.
boolean
().
optional
(),
selectedTemplateId
:
z
.
string
().
optional
(),
selectedTemplateId
:
z
.
string
().
optional
(),
enableSupabaseWriteSqlMigration
:
z
.
boolean
().
optional
(),
enableNativeGit
:
z
.
boolean
().
optional
(),
enableNativeGit
:
z
.
boolean
().
optional
(),
...
...
src/supabase_admin/supabase_context.ts
浏览文件 @
382fe9ba
import
{
IS_TEST_BUILD
}
from
"@/ipc/utils/test_utils"
;
import
{
getSupabaseClient
}
from
"./supabase_management_client"
;
import
{
getSupabaseClient
}
from
"./supabase_management_client"
;
import
{
SUPABASE_SCHEMA_QUERY
}
from
"./supabase_schema_query"
;
import
{
SUPABASE_SCHEMA_QUERY
}
from
"./supabase_schema_query"
;
async
function
getPublishableKey
({
projectId
}:
{
projectId
:
string
})
{
async
function
getPublishableKey
({
projectId
}:
{
projectId
:
string
})
{
if
(
IS_TEST_BUILD
)
{
return
"test-publishable-key"
;
}
const
supabase
=
await
getSupabaseClient
();
const
supabase
=
await
getSupabaseClient
();
let
keys
;
let
keys
;
try
{
try
{
...
@@ -50,6 +55,10 @@ export async function getSupabaseContext({
...
@@ -50,6 +55,10 @@ export async function getSupabaseContext({
}:
{
}:
{
supabaseProjectId
:
string
;
supabaseProjectId
:
string
;
})
{
})
{
if
(
IS_TEST_BUILD
)
{
return
"[[TEST_BUILD_SUPABASE_CONTEXT]]"
;
}
const
supabase
=
await
getSupabaseClient
();
const
supabase
=
await
getSupabaseClient
();
const
publishableKey
=
await
getPublishableKey
({
const
publishableKey
=
await
getPublishableKey
({
projectId
:
supabaseProjectId
,
projectId
:
supabaseProjectId
,
...
...
src/supabase_admin/supabase_management_client.ts
浏览文件 @
382fe9ba
...
@@ -140,6 +140,10 @@ export async function executeSupabaseSql({
...
@@ -140,6 +140,10 @@ export async function executeSupabaseSql({
supabaseProjectId
:
string
;
supabaseProjectId
:
string
;
query
:
string
;
query
:
string
;
}):
Promise
<
string
>
{
}):
Promise
<
string
>
{
if
(
IS_TEST_BUILD
)
{
return
"{}"
;
}
const
supabase
=
await
getSupabaseClient
();
const
supabase
=
await
getSupabaseClient
();
const
result
=
await
supabase
.
runQuery
(
supabaseProjectId
,
query
);
const
result
=
await
supabase
.
runQuery
(
supabaseProjectId
,
query
);
return
JSON
.
stringify
(
result
);
return
JSON
.
stringify
(
result
);
...
...
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论