Skip to content

Commit 02f6f03

Browse files
committed
feat: Implement UI and API for inserting rows into tables.
1 parent 1cf49f9 commit 02f6f03

3 files changed

Lines changed: 168 additions & 1 deletion

File tree

api/routes.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
import { decodeSession, decryptMessage, encryptMessage } from '/api/user.ts'
3535
import {
3636
fetchTablesData,
37+
insertTableData,
3738
runSQL,
3839
SQLQueryError,
3940
updateTableData,
@@ -527,6 +528,19 @@ const defs = {
527528
rows: ARR(OBJ({}, 'A row of the result set'), 'The result set rows'),
528529
}),
529530
}),
531+
'POST/api/deployment/table/insert': route({
532+
authorize: withUserSession,
533+
fn: (ctx, { deployment, table, data }) => {
534+
const dep = withDeploymentTableAccess(ctx, deployment)
535+
return insertTableData(dep, table, data)
536+
},
537+
input: OBJ({
538+
deployment: STR("The deployment's URL"),
539+
table: STR('The table name'),
540+
data: OBJ({}, 'The row data to insert'),
541+
}),
542+
output: OBJ({}, 'The result of the insert'),
543+
}),
530544
'POST/api/deployment/table/update': route({
531545
authorize: withUserSession,
532546
fn: (ctx, { deployment, table, pk, data }) => {

api/sql.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,44 @@ export const fetchTablesData = async (
268268
}
269269
}
270270

271+
export const insertTableData = async (
272+
deployment: Deployment,
273+
table: string,
274+
data: Record<string, unknown>,
275+
) => {
276+
const { sqlEndpoint, sqlToken } = deployment
277+
if (!sqlToken || !sqlEndpoint) {
278+
throw Error('Missing SQL endpoint or token')
279+
}
280+
const projectFunctions = getProjectFunctions(deployment.projectId)
281+
const transformedData = await applyWriteTransformers(
282+
data,
283+
deployment.projectId,
284+
deployment.url,
285+
table,
286+
projectFunctions,
287+
)
288+
const columns = Object.keys(transformedData)
289+
const values = Object.values(transformedData).map((v) => {
290+
if (v === null) return 'NULL'
291+
if (typeof v === 'string') return `'${v.replace(/'/g, "''")}'`
292+
return String(v)
293+
})
294+
const query = `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${
295+
values.join(', ')
296+
})`
297+
const rows = await runSQL(sqlEndpoint, sqlToken, query)
298+
299+
// Apply read transformer pipeline
300+
return await applyReadTransformers(
301+
rows,
302+
deployment.projectId,
303+
deployment.url,
304+
table,
305+
projectFunctions,
306+
)
307+
}
308+
271309
export const updateTableData = async (
272310
deployment: Deployment,
273311
table: string,

web/pages/DeploymentPage.tsx

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1717,10 +1717,125 @@ const LogDetails = () => {
17171717
)
17181718
}
17191719

1720+
const InsertRow = () => {
1721+
const tableName = url.params.table || schema.data?.tables?.[0]?.table
1722+
const tableDef = schema.data?.tables?.find((t) => t.table === tableName)
1723+
1724+
if (!tableName || !tableDef) {
1725+
return (
1726+
<div class='p-4 text-base-content/60'>
1727+
Select a table from the schema panel first.
1728+
</div>
1729+
)
1730+
}
1731+
1732+
const onInsert = async (e: Event) => {
1733+
e.preventDefault()
1734+
const form = e.currentTarget as HTMLFormElement
1735+
const formData = new FormData(form)
1736+
const data: Record<string, unknown> = {}
1737+
1738+
for (const [key, val] of formData.entries()) {
1739+
const col = tableDef.columns.find((c) => c.name === key)
1740+
if (!col) continue
1741+
const type = col.type
1742+
if (
1743+
type.includes('Int') || type.includes('Float') ||
1744+
type.includes('Decimal')
1745+
) {
1746+
data[key] = Number(val)
1747+
} else if (type.includes('Bool')) {
1748+
data[key] = val === 'on'
1749+
} else if (
1750+
type.includes('JSON') || type.includes('Array') || type.includes('Map')
1751+
) {
1752+
try {
1753+
data[key] = JSON.parse(val as string)
1754+
} catch {
1755+
data[key] = val
1756+
}
1757+
} else {
1758+
data[key] = val
1759+
}
1760+
}
1761+
1762+
try {
1763+
await api['POST/api/deployment/table/insert'].fetch({
1764+
deployment: url.params.dep!,
1765+
table: tableName,
1766+
data,
1767+
})
1768+
toast('Row inserted successfully')
1769+
tableData.fetch()
1770+
navigate({ params: { drawer: null } })
1771+
} catch (err) {
1772+
toast(err instanceof Error ? err.message : String(err), 'error')
1773+
}
1774+
}
1775+
1776+
return (
1777+
<div class='flex flex-col h-full bg-base-100'>
1778+
<div class='p-4 border-b border-base-300 flex items-center justify-between sticky top-0 bg-base-100 z-10'>
1779+
<h3 class='font-semibold text-lg'>Insert Row: {tableName}</h3>
1780+
<A
1781+
params={{ drawer: null }}
1782+
replace
1783+
class='btn btn-ghost btn-sm btn-circle'
1784+
>
1785+
<XCircle class='h-5 w-5' />
1786+
</A>
1787+
</div>
1788+
<form onSubmit={onInsert} class='flex-1 flex flex-col min-h-0'>
1789+
<div class='flex-1 overflow-y-auto p-4 space-y-4'>
1790+
{tableDef.columns.map((col) => {
1791+
const type = col.type
1792+
const key = col.name
1793+
const isObject = type.includes('Map') || type.includes('Array') ||
1794+
type.includes('Tuple') || type.includes('Nested') ||
1795+
type.includes('JSON') || type.toLowerCase().includes('blob')
1796+
const isNumber = type.includes('Int') || type.includes('Float') ||
1797+
type.includes('Decimal')
1798+
const isBoolean = type.includes('Bool')
1799+
const isDate = type.includes('Date') || type.includes('Time')
1800+
1801+
return (
1802+
<div key={key} class='form-control'>
1803+
<label class='label py-1'>
1804+
<span class='label-text text-xs font-semibold text-base-content/50 uppercase tracking-wider'>
1805+
{key}
1806+
</span>
1807+
<span class='label-text-alt text-[10px] opacity-50'>
1808+
{type}
1809+
</span>
1810+
</label>
1811+
{isObject
1812+
? <ObjectInput name={key} />
1813+
: isBoolean
1814+
? <BooleanInput name={key} />
1815+
: isDate
1816+
? <DateInput name={key} />
1817+
: isNumber
1818+
? <NumberInput name={key} />
1819+
: <TextInput name={key} />}
1820+
</div>
1821+
)
1822+
})}
1823+
</div>
1824+
<div class='p-4 border-t border-base-300 sticky bottom-0 bg-base-100'>
1825+
<button type='submit' class='btn btn-primary w-full'>
1826+
<Plus class='h-4 w-4' />
1827+
Insert Row
1828+
</button>
1829+
</div>
1830+
</form>
1831+
</div>
1832+
)
1833+
}
1834+
17201835
type DrawerTab = 'history' | 'insert' | 'view-row' | 'view-log'
17211836
const drawerViews: Record<DrawerTab, JSX.Element> = {
17221837
history: <QueryHistory />,
1723-
insert: <div></div>,
1838+
insert: <InsertRow />,
17241839
'view-row': <RowDetails />,
17251840
'view-log': <LogDetails />,
17261841
} as const

0 commit comments

Comments
 (0)