diff --git a/api/routes.ts b/api/routes.ts index 407e71a..bc4850e 100644 --- a/api/routes.ts +++ b/api/routes.ts @@ -6,6 +6,8 @@ import { DeploymentDef, DeploymentsCollection, ProjectsCollection, + SQLToolDef, + SQLToolsCollection, TeamDef, TeamsCollection, UserDef, @@ -238,6 +240,54 @@ const defs = { output: BOOL('Indicates if the project was deleted'), description: 'Delete a project by ID', }), + 'GET/api/project/tools': route({ + authorize: withUserSession, + fn: (_ctx, { project }) => { + const tools = SQLToolsCollection.filter((t) => t.projectId === project) + return tools + }, + input: OBJ({ project: STR('The ID of the project') }), + output: ARR(SQLToolDef, 'List of SQL tools'), + description: 'Get SQL tools for a project', + }), + 'POST/api/project/tool': route({ + authorize: withAdminSession, + fn: (_ctx, input) => { + const toolId = crypto.randomUUID() + const tool = { ...input, toolId } + SQLToolsCollection.insert(tool) + return tool + }, + input: OBJ({ + projectId: STR('The ID of the project'), + name: STR('The name of the tool'), + targetTables: ARR( + STR('Target table names or *'), + 'List of target tables', + ), + targetColumns: ARR( + STR('Target column names or *'), + 'List of target columns', + ), + triggerEvent: LIST(['BEFORE', 'AFTER'], 'Trigger event: BEFORE or AFTER'), + code: STR('The JS function body'), + enabled: BOOL('Is the tool enabled?'), + }), + output: SQLToolDef, + description: 'Create a new SQL tool', + }), + 'DELETE/api/project/tool': route({ + authorize: withAdminSession, + fn: (_ctx, { id }) => { + const tool = SQLToolsCollection.get(id) + if (!tool) throw respond.NotFound({ message: 'Tool not found' }) + SQLToolsCollection.delete(id) + return true + }, + input: OBJ({ id: STR('The ID of the tool') }), + output: BOOL('Indicates if the tool was deleted'), + description: 'Delete a SQL tool', + }), 'GET/api/project/deployments': route({ authorize: withUserSession, fn: (_ctx, { project }) => { diff --git a/api/schema.ts b/api/schema.ts index 7b10848..7424e30 100644 --- a/api/schema.ts +++ b/api/schema.ts @@ -2,6 +2,7 @@ import { ARR, type Asserted, BOOL, + LIST, NUM, OBJ, optional, @@ -92,3 +93,20 @@ export const DatabaseSchemasCollection = await createCollection< DatabaseSchema, 'deploymentUrl' >({ name: 'db_schemas', primaryKey: 'deploymentUrl' }) + +export const SQLToolDef = OBJ({ + toolId: STR('The unique identifier for the tool'), + projectId: STR('The ID of the project this tool belongs to'), + name: STR('The name of the tool'), + targetTables: ARR(STR('Target table names or *'), 'List of target tables'), + targetColumns: ARR(STR('Target column names or *'), 'List of target columns'), + triggerEvent: LIST(['BEFORE', 'AFTER'], 'Trigger event: BEFORE or AFTER'), + code: STR('The JS function body'), + enabled: BOOL('Is the tool enabled?'), +}, 'The SQL tool definition') +export type SQLTool = Asserted + +export const SQLToolsCollection = await createCollection({ + name: 'sql_tools', + primaryKey: 'toolId', +}) diff --git a/web/components/Dialog.tsx b/web/components/Dialog.tsx index e21f195..70dac1b 100644 --- a/web/components/Dialog.tsx +++ b/web/components/Dialog.tsx @@ -44,10 +44,12 @@ export const Dialog = ({ ) } -export const DialogModal = ({ children, ...props }: DialogProps) => { +export const DialogModal = ( + { children, boxClass, ...props }: DialogProps & { boxClass?: string }, +) => { return ( - + ) +} + +const ToolList = () => ( +
+
+

+ Configured Tools ({tools.data?.length}) +

+
+ {tools.data?.length === 0 + ? ( +
+ +

No tools configured.

+ + Create your first tool + +
+ ) + : ( +
+ {tools.data?.map((tool) => ( + + ))} +
+ )} +
+) + +const onSubmit = async (e: TargetedEvent) => { + e.preventDefault() + if (!project.data?.slug) return + + const form = e.currentTarget + const formData = new FormData(form) + const name = formData.get('name') as string + const triggerEvent = formData.get('triggerEvent') as 'BEFORE' | 'AFTER' + const targetTables = (formData.get('targetTables') as string).split(',').map(s => s.trim()).filter(Boolean) + const targetColumns = (formData.get('targetColumns') as string).split(',').map(s => s.trim()).filter(Boolean) + const code = formData.get('code') as string + const enabled = formData.get('enabled') === 'on' + + await createTool.fetch({ + projectId: project.data.slug, + name, + triggerEvent, + targetTables, + targetColumns, + code, + enabled + }) + + if (!createTool.error) { + navigate({params: {dialog: null}}) + tools.fetch({project: project.data.slug}) + } +} + +const CreateToolModal = () => ( + +
+
+

+ + Create SQL Tool +

+
+ + +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ +
+ + +
+
+ + + +
+
+ ) + +const ToolsSidebar = () => { + const activeTab = url.params.tab + if (!activeTab) { + navigate({ params: { tab: 'sql' }, replace: true }) + return null + } + return ( + + ) +} + +export function ToolsPage() { + return ( +
+ +
+ + New Tool + + } + /> + +
+
+ {tools.pending && tools.data?.length === 0 + ? ( +
+ +
+ ) + : } +
+
+ + +
+
+ ) +}