From 2f90947fdd4ae0152dbef1c12148de5535cf90c3 Mon Sep 17 00:00:00 2001 From: Abdou TOP Date: Wed, 25 Feb 2026 11:11:27 +0000 Subject: [PATCH 1/5] feat: Add functionality to edit and update table rows on the deployment page, including new SQL methods and toast feedback. --- api/routes.ts | 14 +++++ api/sql.ts | 41 ++++++++++++- web/pages/DeploymentPage.tsx | 115 ++++++++++++++++++++++++++++++++++- 3 files changed, 165 insertions(+), 5 deletions(-) diff --git a/api/routes.ts b/api/routes.ts index 65fa0c0..ea9607f 100644 --- a/api/routes.ts +++ b/api/routes.ts @@ -34,6 +34,7 @@ import { import { decodeSession, decryptMessage, encryptMessage } from '/api/user.ts' import { fetchTablesData, + insertTableData, runSQL, SQLQueryError, updateTableData, @@ -527,6 +528,19 @@ const defs = { rows: ARR(OBJ({}, 'A row of the result set'), 'The result set rows'), }), }), + 'POST/api/deployment/table/insert': route({ + authorize: withUserSession, + fn: (ctx, { deployment, table, data }) => { + const dep = withDeploymentTableAccess(ctx, deployment) + return insertTableData(dep, table, data) + }, + input: OBJ({ + deployment: STR("The deployment's URL"), + table: STR('The table name'), + data: OBJ({}, 'The row data to insert'), + }), + output: OBJ({}, 'The result of the insert'), + }), 'POST/api/deployment/table/update': route({ authorize: withUserSession, fn: (ctx, { deployment, table, pk, data }) => { diff --git a/api/sql.ts b/api/sql.ts index d337595..8b8a130 100644 --- a/api/sql.ts +++ b/api/sql.ts @@ -268,6 +268,45 @@ export const fetchTablesData = async ( } } +export const insertTableData = async ( + deployment: Deployment, + table: string, + data: Record, +) => { + const { sqlEndpoint, sqlToken } = deployment + if (!sqlToken || !sqlEndpoint) { + throw Error('Missing SQL endpoint or token') + } + const projectFunctions = getProjectFunctions(deployment.projectId) + + const transformedData = await applyWriteTransformers( + data, + deployment.projectId, + deployment.url, + table, + projectFunctions, + ) + + const columns = Object.keys(transformedData) + const values = Object.values(transformedData).map((v) => { + if (v === null) return 'NULL' + if (typeof v === 'string') return `'${v.replace(/'/g, "''")}'` + return String(v) + }) + const query = + `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${values.join(', ')})` + const rows = await runSQL(sqlEndpoint, sqlToken, query) + + // Apply read transformer pipeline + return await applyReadTransformers( + rows, + deployment.projectId, + deployment.url, + table, + projectFunctions, + ) +} + export const updateTableData = async ( deployment: Deployment, table: string, @@ -275,7 +314,6 @@ export const updateTableData = async ( data: Record, ) => { const { sqlEndpoint, sqlToken } = deployment - if (!sqlToken || !sqlEndpoint) { throw Error('Missing SQL endpoint or token') } @@ -297,7 +335,6 @@ export const updateTableData = async ( : String(v) return `${k} = ${val}` }) - const pkVal = typeof pk.value === 'string' ? `'${String(pk.value).replace(/'/g, "''")}'` : String(pk.value) diff --git a/web/pages/DeploymentPage.tsx b/web/pages/DeploymentPage.tsx index c4f4648..07a39e2 100644 --- a/web/pages/DeploymentPage.tsx +++ b/web/pages/DeploymentPage.tsx @@ -1430,8 +1430,7 @@ const RowDetails = () => {
{Object.entries(row).map(([key, value]) => { - const tableName = url.params.table || - schema.data?.tables?.[0]?.table + const tableName = url.params.table || schema.data?.tables?.[0]?.table const tableDef = schema.data?.tables?.find((t) => t.table === tableName ) @@ -1717,10 +1716,120 @@ const LogDetails = () => { ) } +const InsertRow = () => { + const tableName = url.params.table || schema.data?.tables?.[0]?.table + const tableDef = schema.data?.tables?.find((t) => t.table === tableName) + + if (!tableDef) { + return ( +
+ Select a table from the schema panel first. +
+ ) + } + + const onInsert = async (e: Event) => { + e.preventDefault() + const form = e.currentTarget as HTMLFormElement + const formData = new FormData(form) + const data: Record = {} + + for (const [key, val] of formData.entries()) { + const col = tableDef.columns.find((c) => c.name === key) + if (!col) continue + const type = col.type + if (type.includes('Int') || type.includes('Float') || type.includes('Decimal')) { + data[key] = Number(val) + } else if (type.includes('Bool')) { + data[key] = val === 'on' + } else if (type.includes('JSON') || type.includes('Array') || type.includes('Map')) { + try { + data[key] = JSON.parse(val as string) + } catch { + data[key] = val + } + } else { + data[key] = val + } + } + + try { + await api['POST/api/deployment/table/insert'].fetch({ + deployment: url.params.dep!, + table: tableName, + data, + }) + toast('Row inserted successfully') + tableData.fetch() + navigate({ params: { drawer: null } }) + } catch (err) { + toast(err instanceof Error ? err.message : String(err), 'error') + } + } + + return ( +
+
+

Insert Row: {tableName}

+ + + +
+ +
+ {tableDef.columns.map((col) => { + const type = col.type + const key = col.name + const isObject = type.includes('Map') || type.includes('Array') || + type.includes('Tuple') || type.includes('Nested') || + type.includes('JSON') || type.toLowerCase().includes('blob') + const isNumber = type.includes('Int') || type.includes('Float') || + type.includes('Decimal') + const isBoolean = type.includes('Bool') + const isDate = type.includes('Date') || type.includes('Time') + + return ( +
+ + {isObject + ? + : isBoolean + ? + : isDate + ? + : isNumber + ? + : } +
+ ) + })} +
+
+ +
+ +
+ ) +} + type DrawerTab = 'history' | 'insert' | 'view-row' | 'view-log' const drawerViews: Record = { history: , - insert:
, + insert: , 'view-row': , 'view-log': , } as const From af825eb0fed905fa52cb0592573aa29d85a18f45 Mon Sep 17 00:00:00 2001 From: Abdou TOP Date: Wed, 4 Mar 2026 10:13:18 +0000 Subject: [PATCH 2/5] feat: Introduce data transformation pipelines for SQL operations, expand primary key type support, and enhance table data editing in the UI. --- api/sql.ts | 7 +++++-- web/pages/DeploymentPage.tsx | 14 ++++++++++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/api/sql.ts b/api/sql.ts index 8b8a130..1466705 100644 --- a/api/sql.ts +++ b/api/sql.ts @@ -293,8 +293,9 @@ export const insertTableData = async ( if (typeof v === 'string') return `'${v.replace(/'/g, "''")}'` return String(v) }) - const query = - `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${values.join(', ')})` + const query = `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${ + values.join(', ') + })` const rows = await runSQL(sqlEndpoint, sqlToken, query) // Apply read transformer pipeline @@ -314,6 +315,7 @@ export const updateTableData = async ( data: Record, ) => { const { sqlEndpoint, sqlToken } = deployment + if (!sqlToken || !sqlEndpoint) { throw Error('Missing SQL endpoint or token') } @@ -335,6 +337,7 @@ export const updateTableData = async ( : String(v) return `${k} = ${val}` }) + const pkVal = typeof pk.value === 'string' ? `'${String(pk.value).replace(/'/g, "''")}'` : String(pk.value) diff --git a/web/pages/DeploymentPage.tsx b/web/pages/DeploymentPage.tsx index 07a39e2..ace3bc9 100644 --- a/web/pages/DeploymentPage.tsx +++ b/web/pages/DeploymentPage.tsx @@ -1430,7 +1430,8 @@ const RowDetails = () => {
{Object.entries(row).map(([key, value]) => { - const tableName = url.params.table || schema.data?.tables?.[0]?.table + const tableName = url.params.table || + schema.data?.tables?.[0]?.table const tableDef = schema.data?.tables?.find((t) => t.table === tableName ) @@ -1720,7 +1721,7 @@ const InsertRow = () => { const tableName = url.params.table || schema.data?.tables?.[0]?.table const tableDef = schema.data?.tables?.find((t) => t.table === tableName) - if (!tableDef) { + if (!tableName || !tableDef) { return (
Select a table from the schema panel first. @@ -1738,11 +1739,16 @@ const InsertRow = () => { const col = tableDef.columns.find((c) => c.name === key) if (!col) continue const type = col.type - if (type.includes('Int') || type.includes('Float') || type.includes('Decimal')) { + if ( + type.includes('Int') || type.includes('Float') || + type.includes('Decimal') + ) { data[key] = Number(val) } else if (type.includes('Bool')) { data[key] = val === 'on' - } else if (type.includes('JSON') || type.includes('Array') || type.includes('Map')) { + } else if ( + type.includes('JSON') || type.includes('Array') || type.includes('Map') + ) { try { data[key] = JSON.parse(val as string) } catch { From bca0a0461fd608c8197752ed81e960938d50ca64 Mon Sep 17 00:00:00 2001 From: Abdou TOP Date: Tue, 10 Mar 2026 17:01:24 +0000 Subject: [PATCH 3/5] feat: remove insert row feature by deleting its API route, SQL logic, and UI component. --- api/routes.ts | 14 ----- api/sql.ts | 40 ------------ web/pages/DeploymentPage.tsx | 117 +---------------------------------- 3 files changed, 1 insertion(+), 170 deletions(-) diff --git a/api/routes.ts b/api/routes.ts index ea9607f..65fa0c0 100644 --- a/api/routes.ts +++ b/api/routes.ts @@ -34,7 +34,6 @@ import { import { decodeSession, decryptMessage, encryptMessage } from '/api/user.ts' import { fetchTablesData, - insertTableData, runSQL, SQLQueryError, updateTableData, @@ -528,19 +527,6 @@ const defs = { rows: ARR(OBJ({}, 'A row of the result set'), 'The result set rows'), }), }), - 'POST/api/deployment/table/insert': route({ - authorize: withUserSession, - fn: (ctx, { deployment, table, data }) => { - const dep = withDeploymentTableAccess(ctx, deployment) - return insertTableData(dep, table, data) - }, - input: OBJ({ - deployment: STR("The deployment's URL"), - table: STR('The table name'), - data: OBJ({}, 'The row data to insert'), - }), - output: OBJ({}, 'The result of the insert'), - }), 'POST/api/deployment/table/update': route({ authorize: withUserSession, fn: (ctx, { deployment, table, pk, data }) => { diff --git a/api/sql.ts b/api/sql.ts index 1466705..d337595 100644 --- a/api/sql.ts +++ b/api/sql.ts @@ -268,46 +268,6 @@ export const fetchTablesData = async ( } } -export const insertTableData = async ( - deployment: Deployment, - table: string, - data: Record, -) => { - const { sqlEndpoint, sqlToken } = deployment - if (!sqlToken || !sqlEndpoint) { - throw Error('Missing SQL endpoint or token') - } - const projectFunctions = getProjectFunctions(deployment.projectId) - - const transformedData = await applyWriteTransformers( - data, - deployment.projectId, - deployment.url, - table, - projectFunctions, - ) - - const columns = Object.keys(transformedData) - const values = Object.values(transformedData).map((v) => { - if (v === null) return 'NULL' - if (typeof v === 'string') return `'${v.replace(/'/g, "''")}'` - return String(v) - }) - const query = `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${ - values.join(', ') - })` - const rows = await runSQL(sqlEndpoint, sqlToken, query) - - // Apply read transformer pipeline - return await applyReadTransformers( - rows, - deployment.projectId, - deployment.url, - table, - projectFunctions, - ) -} - export const updateTableData = async ( deployment: Deployment, table: string, diff --git a/web/pages/DeploymentPage.tsx b/web/pages/DeploymentPage.tsx index ace3bc9..c4f4648 100644 --- a/web/pages/DeploymentPage.tsx +++ b/web/pages/DeploymentPage.tsx @@ -1717,125 +1717,10 @@ const LogDetails = () => { ) } -const InsertRow = () => { - const tableName = url.params.table || schema.data?.tables?.[0]?.table - const tableDef = schema.data?.tables?.find((t) => t.table === tableName) - - if (!tableName || !tableDef) { - return ( -
- Select a table from the schema panel first. -
- ) - } - - const onInsert = async (e: Event) => { - e.preventDefault() - const form = e.currentTarget as HTMLFormElement - const formData = new FormData(form) - const data: Record = {} - - for (const [key, val] of formData.entries()) { - const col = tableDef.columns.find((c) => c.name === key) - if (!col) continue - const type = col.type - if ( - type.includes('Int') || type.includes('Float') || - type.includes('Decimal') - ) { - data[key] = Number(val) - } else if (type.includes('Bool')) { - data[key] = val === 'on' - } else if ( - type.includes('JSON') || type.includes('Array') || type.includes('Map') - ) { - try { - data[key] = JSON.parse(val as string) - } catch { - data[key] = val - } - } else { - data[key] = val - } - } - - try { - await api['POST/api/deployment/table/insert'].fetch({ - deployment: url.params.dep!, - table: tableName, - data, - }) - toast('Row inserted successfully') - tableData.fetch() - navigate({ params: { drawer: null } }) - } catch (err) { - toast(err instanceof Error ? err.message : String(err), 'error') - } - } - - return ( -
-
-

Insert Row: {tableName}

- - - -
- -
- {tableDef.columns.map((col) => { - const type = col.type - const key = col.name - const isObject = type.includes('Map') || type.includes('Array') || - type.includes('Tuple') || type.includes('Nested') || - type.includes('JSON') || type.toLowerCase().includes('blob') - const isNumber = type.includes('Int') || type.includes('Float') || - type.includes('Decimal') - const isBoolean = type.includes('Bool') - const isDate = type.includes('Date') || type.includes('Time') - - return ( -
- - {isObject - ? - : isBoolean - ? - : isDate - ? - : isNumber - ? - : } -
- ) - })} -
-
- -
- -
- ) -} - type DrawerTab = 'history' | 'insert' | 'view-row' | 'view-log' const drawerViews: Record = { history: , - insert: , + insert:
, 'view-row': , 'view-log': , } as const From b0f21fc4c9e10dbe0ad0b2182a37d63e5ad4ab49 Mon Sep 17 00:00:00 2001 From: Abdou TOP Date: Tue, 10 Mar 2026 17:56:33 +0000 Subject: [PATCH 4/5] feat: Implement UI and API for inserting rows into tables. --- api/routes.ts | 14 +++++ api/sql.ts | 38 ++++++++++++ web/pages/DeploymentPage.tsx | 117 ++++++++++++++++++++++++++++++++++- 3 files changed, 168 insertions(+), 1 deletion(-) diff --git a/api/routes.ts b/api/routes.ts index 65fa0c0..ea9607f 100644 --- a/api/routes.ts +++ b/api/routes.ts @@ -34,6 +34,7 @@ import { import { decodeSession, decryptMessage, encryptMessage } from '/api/user.ts' import { fetchTablesData, + insertTableData, runSQL, SQLQueryError, updateTableData, @@ -527,6 +528,19 @@ const defs = { rows: ARR(OBJ({}, 'A row of the result set'), 'The result set rows'), }), }), + 'POST/api/deployment/table/insert': route({ + authorize: withUserSession, + fn: (ctx, { deployment, table, data }) => { + const dep = withDeploymentTableAccess(ctx, deployment) + return insertTableData(dep, table, data) + }, + input: OBJ({ + deployment: STR("The deployment's URL"), + table: STR('The table name'), + data: OBJ({}, 'The row data to insert'), + }), + output: OBJ({}, 'The result of the insert'), + }), 'POST/api/deployment/table/update': route({ authorize: withUserSession, fn: (ctx, { deployment, table, pk, data }) => { diff --git a/api/sql.ts b/api/sql.ts index d337595..762a714 100644 --- a/api/sql.ts +++ b/api/sql.ts @@ -268,6 +268,44 @@ export const fetchTablesData = async ( } } +export const insertTableData = async ( + deployment: Deployment, + table: string, + data: Record, +) => { + const { sqlEndpoint, sqlToken } = deployment + if (!sqlToken || !sqlEndpoint) { + throw Error('Missing SQL endpoint or token') + } + const projectFunctions = getProjectFunctions(deployment.projectId) + const transformedData = await applyWriteTransformers( + data, + deployment.projectId, + deployment.url, + table, + projectFunctions, + ) + const columns = Object.keys(transformedData) + const values = Object.values(transformedData).map((v) => { + if (v === null) return 'NULL' + if (typeof v === 'string') return `'${v.replace(/'/g, "''")}'` + return String(v) + }) + const query = `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${ + values.join(', ') + })` + const rows = await runSQL(sqlEndpoint, sqlToken, query) + + // Apply read transformer pipeline + return await applyReadTransformers( + rows, + deployment.projectId, + deployment.url, + table, + projectFunctions, + ) +} + export const updateTableData = async ( deployment: Deployment, table: string, diff --git a/web/pages/DeploymentPage.tsx b/web/pages/DeploymentPage.tsx index c4f4648..ace3bc9 100644 --- a/web/pages/DeploymentPage.tsx +++ b/web/pages/DeploymentPage.tsx @@ -1717,10 +1717,125 @@ const LogDetails = () => { ) } +const InsertRow = () => { + const tableName = url.params.table || schema.data?.tables?.[0]?.table + const tableDef = schema.data?.tables?.find((t) => t.table === tableName) + + if (!tableName || !tableDef) { + return ( +
+ Select a table from the schema panel first. +
+ ) + } + + const onInsert = async (e: Event) => { + e.preventDefault() + const form = e.currentTarget as HTMLFormElement + const formData = new FormData(form) + const data: Record = {} + + for (const [key, val] of formData.entries()) { + const col = tableDef.columns.find((c) => c.name === key) + if (!col) continue + const type = col.type + if ( + type.includes('Int') || type.includes('Float') || + type.includes('Decimal') + ) { + data[key] = Number(val) + } else if (type.includes('Bool')) { + data[key] = val === 'on' + } else if ( + type.includes('JSON') || type.includes('Array') || type.includes('Map') + ) { + try { + data[key] = JSON.parse(val as string) + } catch { + data[key] = val + } + } else { + data[key] = val + } + } + + try { + await api['POST/api/deployment/table/insert'].fetch({ + deployment: url.params.dep!, + table: tableName, + data, + }) + toast('Row inserted successfully') + tableData.fetch() + navigate({ params: { drawer: null } }) + } catch (err) { + toast(err instanceof Error ? err.message : String(err), 'error') + } + } + + return ( +
+
+

Insert Row: {tableName}

+ + + +
+
+
+ {tableDef.columns.map((col) => { + const type = col.type + const key = col.name + const isObject = type.includes('Map') || type.includes('Array') || + type.includes('Tuple') || type.includes('Nested') || + type.includes('JSON') || type.toLowerCase().includes('blob') + const isNumber = type.includes('Int') || type.includes('Float') || + type.includes('Decimal') + const isBoolean = type.includes('Bool') + const isDate = type.includes('Date') || type.includes('Time') + + return ( +
+ + {isObject + ? + : isBoolean + ? + : isDate + ? + : isNumber + ? + : } +
+ ) + })} +
+
+ +
+
+
+ ) +} + type DrawerTab = 'history' | 'insert' | 'view-row' | 'view-log' const drawerViews: Record = { history: , - insert:
, + insert: , 'view-row': , 'view-log': , } as const From c1534ee0527a1dfeb0b4aa4b6322f37e99e8ff72 Mon Sep 17 00:00:00 2001 From: Abdou TOP Date: Thu, 12 Mar 2026 23:54:45 +0000 Subject: [PATCH 5/5] refactor: use String.prototype.replaceAll for SQL string escaping --- api/sql.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/sql.ts b/api/sql.ts index 762a714..a81da83 100644 --- a/api/sql.ts +++ b/api/sql.ts @@ -288,7 +288,7 @@ export const insertTableData = async ( const columns = Object.keys(transformedData) const values = Object.values(transformedData).map((v) => { if (v === null) return 'NULL' - if (typeof v === 'string') return `'${v.replace(/'/g, "''")}'` + if (typeof v === 'string') return `'${v.replaceAll("'", "''")}'` return String(v) }) const query = `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${ @@ -331,13 +331,13 @@ export const updateTableData = async ( const val = v === null ? 'NULL' : typeof v === 'string' - ? `'${v.replace(/'/g, "''")}'` + ? `'${v.replaceAll("'", "''")}'` : String(v) return `${k} = ${val}` }) const pkVal = typeof pk.value === 'string' - ? `'${String(pk.value).replace(/'/g, "''")}'` + ? `'${String(pk.value).replaceAll("'", "''")}'` : String(pk.value) const query = `UPDATE ${table} SET ${