diff --git a/frontend/src/components/Editor/EditorComponents/GenerateCode.jsx b/frontend/src/components/Editor/EditorComponents/GenerateCode.jsx index c83d8bcd..dedc6f63 100644 --- a/frontend/src/components/Editor/EditorComponents/GenerateCode.jsx +++ b/frontend/src/components/Editor/EditorComponents/GenerateCode.jsx @@ -2,71 +2,71 @@ import { useState } from 'react'; const GenerateCode = () => { - const [contractFilter, setContractFilter] = useState(''); - const [selectedMethods, setSelectedMethods] = useState([]); - const [selectedEvents, setSelectedEvents] = useState([]); - const [generatedCode, setGeneratedCode] = useState({ jsCode: '', sqlCode: '' }); + const [contractFilter, setContractFilter] = useState(''); + const [selectedMethods, setSelectedMethods] = useState([]); + const [selectedEvents, setSelectedEvents] = useState([]); + const [generatedCode, setGeneratedCode] = useState({ jsCode: '', sqlCode: '' }); - const handleGenerateCode = async () => { - const response = await fetch('/api/generateCode', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ contractFilter, selectedMethods, selectedEvents }), - }); - const data = await response.json(); - setGeneratedCode(data); - }; + const handleGenerateCode = async () => { + const response = await fetch('/api/generateCode', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ contractFilter, selectedMethods, selectedEvents }), + }); + const data = await response.json(); + setGeneratedCode(data); + }; - return ( -
-
-

Generate Code

-
- setContractFilter(e.target.value)} - className="w-full p-3 border border-gray-300 rounded-md" - /> -
-
- setSelectedMethods(e.target.value.split(','))} - className="w-full p-3 border border-gray-300 rounded-md" - /> -
-
- setSelectedEvents(e.target.value.split(','))} - className="w-full p-3 border border-gray-300 rounded-md" - /> -
- -
-

Generated JavaScript Code

-
{generatedCode.jsCode}
-
-
-

Generated SQL Code

-
{generatedCode.sqlCode}
-
-
+ return ( +
+
+

Generate Code

+
+ setContractFilter(e.target.value)} + className="w-full p-3 border border-gray-300 rounded-md" + />
- ); +
+ setSelectedMethods(e.target.value.split(','))} + className="w-full p-3 border border-gray-300 rounded-md" + /> +
+
+ setSelectedEvents(e.target.value.split(','))} + className="w-full p-3 border border-gray-300 rounded-md" + /> +
+ +
+

Generated JavaScript Code

+
{generatedCode.jsCode}
+
+
+

Generated SQL Code

+
{generatedCode.sqlCode}
+
+
+
+ ); }; export default GenerateCode; diff --git a/frontend/src/pages/api/generateCode.js b/frontend/src/pages/api/generateCode.js deleted file mode 100644 index 44d121ce..00000000 --- a/frontend/src/pages/api/generateCode.js +++ /dev/null @@ -1,71 +0,0 @@ -import { defaultCode, defaultSchema, } from '../../utils/formatters'; - -export default function handler(req, res) { - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'POST'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); - - if (req.method === 'OPTIONS') { - res.status(200).end(); - return; - } - - const { contractFilter, selectedMethods, selectedEvents } = req.body; - - if (!contractFilter || !selectedMethods || !selectedEvents) { - return res.status(400).json({ error: 'Missing required fields' }); - } - - if (!Array.isArray(selectedMethods) || !Array.isArray(selectedEvents)) { - return res.status(400).json({ error: 'selectedMethods and selectedEvents must be arrays' }); - } - - const jsCode = generateDummyJSCode(contractFilter, selectedMethods, selectedEvents); - const sqlCode = generateDummySQLCode(contractFilter, selectedMethods, selectedEvents); - - res.status(200).json({ jsCode, sqlCode }); -} - -function generateDummyJSCode(contractFilter, selectedMethods, selectedEvents) { - let jsCode = `// JavaScript Code\n\n`; - jsCode += `-- Contract Filter: ${contractFilter}\n\n`; - jsCode += `-- Selected Methods: ${selectedMethods}\n\n`; - jsCode += `-- Selected Events: ${selectedEvents}\n\n`; - - jsCode += defaultCode; - - selectedMethods.forEach(method => { - jsCode += `function ${method}() {\n`; - jsCode += ` console.log('Executing ${method}');\n`; - jsCode += `}\n\n`; - }); - - selectedEvents.forEach(event => { - jsCode += `function handle${event}() {\n`; - jsCode += ` console.log('Handling event ${event}');\n`; - jsCode += `}\n\n`; - }); - - return jsCode; -} - -function generateDummySQLCode(contractFilter, selectedMethods, selectedEvents) { - let sqlCode = `-- SQL Code\n\n`; - sqlCode += `-- Contract Filter: ${contractFilter}\n\n`; - sqlCode += `-- Selected Methods: ${selectedMethods}\n\n`; - sqlCode += `-- Selected Events: ${selectedEvents}\n\n`; - - sqlCode += defaultSchema; - - selectedMethods.forEach(method => { - sqlCode += `-- Method: ${method}\n`; - sqlCode += `INSERT INTO methods (name) VALUES ('${method}');\n\n`; - }); - - selectedEvents.forEach(event => { - sqlCode += `-- Event: ${event}\n`; - sqlCode += `INSERT INTO events (name) VALUES ('${event}');\n\n`; - }); - - return sqlCode; -} diff --git a/frontend/src/pages/api/generateCode.ts b/frontend/src/pages/api/generateCode.ts new file mode 100644 index 00000000..50c0d8bd --- /dev/null +++ b/frontend/src/pages/api/generateCode.ts @@ -0,0 +1,98 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; + +import { defaultCode, defaultSchema } from '../../utils/formatters'; + +interface RequestBody { + contractFilter: string | string[]; + selectedMethods: string[]; + selectedEvents: string[]; +} + +const validateRequestBody = (body: any): body is RequestBody => { + const isStringOrArray = (value: any): value is string | string[] => + typeof value === 'string' || (Array.isArray(value) && value.every((item) => typeof item === 'string')); + + return ( + isStringOrArray(body.contractFilter) && + Array.isArray(body.selectedMethods) && + body.selectedMethods.every((method: any) => typeof method === 'string') && + Array.isArray(body.selectedEvents) && + body.selectedEvents.every((event: any) => typeof event === 'string') + ); +}; + +const generateDummyJSCode = ( + contractFilter: string | string[], + selectedMethods: string[], + selectedEvents: string[], +): string => { + const filterString = Array.isArray(contractFilter) ? contractFilter.join(', ') : contractFilter; + const jsCodeHeader = + `// JavaScript Code\n\n` + + `-- Contract Filter: ${filterString}\n\n` + + `-- Selected Methods: ${selectedMethods.join(', ')}\n\n` + + `-- Selected Events: ${selectedEvents.join(', ')}\n\n`; + + const methodsJS = selectedMethods + .map((method) => `function ${method}() {\n console.log('Executing ${method}');\n}\n\n`) + .join(''); + + const eventsJS = selectedEvents + .map((event) => `function handle${event}() {\n console.log('Handling event ${event}');\n}\n\n`) + .join(''); + + return jsCodeHeader + defaultCode + methodsJS + eventsJS; +}; + +const generateDummySQLCode = ( + contractFilter: string | string[], + selectedMethods: string[], + selectedEvents: string[], +): string => { + const filterString = Array.isArray(contractFilter) ? contractFilter.join(', ') : contractFilter; + const sqlCodeHeader = + `-- SQL Code\n\n` + + `-- Contract Filter: ${filterString}\n\n` + + `-- Selected Methods: ${selectedMethods.join(', ')}\n\n` + + `-- Selected Events: ${selectedEvents.join(', ')}\n\n`; + + const methodsSQL = selectedMethods + .map((method) => `-- Method: ${method}\nINSERT INTO methods (name) VALUES ('${method}');\n\n`) + .join(''); + + const eventsSQL = selectedEvents + .map((event) => `-- Event: ${event}\nINSERT INTO events (name) VALUES ('${event}');\n\n`) + .join(''); + + return sqlCodeHeader + defaultSchema + methodsSQL + eventsSQL; +}; + +export default function handler(req: NextApiRequest, res: NextApiResponse): void { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'POST'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + + if (req.method === 'OPTIONS') { + res.status(200).end(); + return; + } + if (req.method !== 'POST') { + res.status(405).json({ error: 'Method Not Allowed' }); + return; + } + + if (!validateRequestBody(req.body)) { + res.status(400).json({ + error: + 'Invalid request body: contractFilter must be a string or an array of strings, and selectedMethods and selectedEvents must be arrays of strings', + }); + return; + } + + const { contractFilter, selectedMethods, selectedEvents } = req.body; + + const jsCode = generateDummyJSCode(contractFilter, selectedMethods, selectedEvents); + const sqlCode = generateDummySQLCode(contractFilter, selectedMethods, selectedEvents); + + res.status(200).json({ jsCode, sqlCode }); +} diff --git a/frontend/src/pages/query-api-editor/index.js b/frontend/src/pages/query-api-editor/index.js index 32e2d2d1..87750c60 100644 --- a/frontend/src/pages/query-api-editor/index.js +++ b/frontend/src/pages/query-api-editor/index.js @@ -3,9 +3,9 @@ import React, { useContext, useEffect } from 'react'; import { Alert } from 'react-bootstrap'; import Editor from '@/components/Editor/EditorComponents/Editor'; +import GenerateCode from '@/components/Editor/EditorComponents/GenerateCode'; import IndexerLogsContainer from '@/components/Logs/LogsViewContainer/IndexerLogsContainer'; import { IndexerDetailsContext } from '@/contexts/IndexerDetailsContext'; -import GenerateCode from '@/components/Editor/EditorComponents/GenerateCode'; const QueryApiEditorPage = ({ router }) => { const { accountId, indexerName } = router.query; @@ -26,9 +26,7 @@ const QueryApiEditorPage = ({ router }) => { } if (accountId == 'test' || indexerName == 'test') { - return ( - - ); + return ; } return showLogsView ? : ; diff --git a/frontend/src/test/api/generateCode.test.js b/frontend/src/test/api/generateCode.test.js index ff3156f0..4fa52f28 100644 --- a/frontend/src/test/api/generateCode.test.js +++ b/frontend/src/test/api/generateCode.test.js @@ -1,66 +1,189 @@ -import handler from '../../pages/api/generateCode'; import { createMocks } from 'node-mocks-http'; -jest.mock('../../utils/formatters', () => ({ - defaultCode: '// Default JS Code', - defaultSchema: '-- Default SQL Schema', -})); +import handler from '../../pages/api/generateCode'; describe('API Handler', () => { - it('should return generated JS and SQL code for valid input', async () => { - const { req, res } = createMocks({ - method: 'POST', - body: { - contractFilter: 'filter', - selectedMethods: ['method1', 'method2'], - selectedEvents: ['event1', 'event2'], - }, - }); + it('should return generated JS and SQL code for valid input', async () => { + const { req, res } = createMocks({ + method: 'POST', + body: { + contractFilter: 'filter', + selectedMethods: ['method1', 'method2'], + selectedEvents: ['event1', 'event2'], + }, + }); + + await handler(req, res); + + expect(res._getStatusCode()).toBe(200); + const responseData = JSON.parse(res._getData()); + expect(responseData).toHaveProperty('jsCode'); + expect(responseData).toHaveProperty('sqlCode'); + }); + + it('should return 400 if required fields are missing', async () => { + const { req, res } = createMocks({ + method: 'POST', + body: {}, + }); + + await handler(req, res); + + expect(res._getStatusCode()).toBe(400); + expect(JSON.parse(res._getData())).toEqual({ + error: + 'Invalid request body: contractFilter must be a string or an array of strings, and selectedMethods and selectedEvents must be arrays of strings', + }); + }); + + it('should return 400 if selectedMethods or selectedEvents are not arrays', async () => { + const { req, res } = createMocks({ + method: 'POST', + body: { + contractFilter: 'filter', + selectedMethods: 'not-an-array', + selectedEvents: [], + }, + }); + + await handler(req, res); + + expect(res._getStatusCode()).toBe(400); + expect(JSON.parse(res._getData())).toEqual({ + error: + 'Invalid request body: contractFilter must be a string or an array of strings, and selectedMethods and selectedEvents must be arrays of strings', + }); + + const { req: req2, res: res2 } = createMocks({ + method: 'POST', + body: { + contractFilter: 'filter', + selectedMethods: [], + selectedEvents: 'not-an-array', + }, + }); + + await handler(req2, res2); - await handler(req, res); + expect(res2._getStatusCode()).toBe(400); + expect(JSON.parse(res2._getData())).toEqual({ + error: + 'Invalid request body: contractFilter must be a string or an array of strings, and selectedMethods and selectedEvents must be arrays of strings', + }); + }); + + it('should handle OPTIONS request correctly', async () => { + const { req, res } = createMocks({ + method: 'OPTIONS', + }); + + await handler(req, res); + + expect(res._getStatusCode()).toBe(200); + }); + + it('should handle empty arrays correctly', async () => { + const { req, res } = createMocks({ + method: 'POST', + body: { + contractFilter: 'filter', + selectedMethods: [], + selectedEvents: [], + }, + }); + + await handler(req, res); + + expect(res._getStatusCode()).toBe(200); + const responseData = JSON.parse(res._getData()); + expect(responseData).toHaveProperty('jsCode'); + expect(responseData).toHaveProperty('sqlCode'); + expect(responseData.jsCode).toContain('// JavaScript Code'); + expect(responseData.sqlCode).toContain('-- SQL Code'); + }); - expect(res._getStatusCode()).toBe(200); + it('should return 400 for invalid contractFilter data type', async () => { + const { req, res } = createMocks({ + method: 'POST', + body: { + contractFilter: 123, + selectedMethods: ['method1'], + selectedEvents: ['event1'], + }, }); - it('should return 400 if required fields are missing', async () => { - const { req, res } = createMocks({ - method: 'POST', - body: {}, - }); + await handler(req, res); - await handler(req, res); + expect(res._getStatusCode()).toBe(400); + expect(JSON.parse(res._getData())).toEqual({ + error: + 'Invalid request body: contractFilter must be a string or an array of strings, and selectedMethods and selectedEvents must be arrays of strings', + }); + }); - expect(res._getStatusCode()).toBe(400); - expect(JSON.parse(res._getData())).toEqual({ error: 'Missing required fields' }); + it('should return 400 for invalid method or event data types', async () => { + const { req, res } = createMocks({ + method: 'POST', + body: { + contractFilter: 'filter', + selectedMethods: [123], + selectedEvents: ['event1'], + }, }); - it('should return 400 if selectedMethods or selectedEvents are not arrays', async () => { - const { req, res } = createMocks({ - method: 'POST', - body: { - contractFilter: 'filter', - selectedMethods: 'not-an-array', - selectedEvents: [], - }, - }); + await handler(req, res); - await handler(req, res); + expect(res._getStatusCode()).toBe(400); + expect(JSON.parse(res._getData())).toEqual({ + error: + 'Invalid request body: contractFilter must be a string or an array of strings, and selectedMethods and selectedEvents must be arrays of strings', + }); + }); - expect(res._getStatusCode()).toBe(400); - expect(JSON.parse(res._getData())).toEqual({ error: 'selectedMethods and selectedEvents must be arrays' }); + it('should handle large inputs correctly', async () => { + const largeArray = Array.from({ length: 1000 }, (_, i) => `method${i}`); + const { req, res } = createMocks({ + method: 'POST', + body: { + contractFilter: 'filter', + selectedMethods: largeArray, + selectedEvents: largeArray, + }, + }); - const { req: req2, res: res2 } = createMocks({ - method: 'POST', - body: { - contractFilter: 'filter', - selectedMethods: [], - selectedEvents: 'not-an-array', - }, - }); + await handler(req, res); - await handler(req2, res2); + expect(res._getStatusCode()).toBe(200); + const responseData = JSON.parse(res._getData()); + expect(responseData).toHaveProperty('jsCode'); + expect(responseData).toHaveProperty('sqlCode'); + }); - expect(res2._getStatusCode()).toBe(400); - expect(JSON.parse(res2._getData())).toEqual({ error: 'selectedMethods and selectedEvents must be arrays' }); + it('should return 405 for unsupported HTTP methods', async () => { + const { req, res } = createMocks({ + method: 'GET', }); + + await handler(req, res); + + expect(res._getStatusCode()).toBe(405); + expect(JSON.parse(res._getData())).toEqual({ error: 'Method Not Allowed' }); + }); + + it('should have correct CORS headers', async () => { + const { req, res } = createMocks({ + method: 'POST', + body: { + contractFilter: 'filter', + selectedMethods: ['method1'], + selectedEvents: ['event1'], + }, + }); + + await handler(req, res); + + expect(res._getHeaders()).toHaveProperty('access-control-allow-origin', '*'); + expect(res._getHeaders()).toHaveProperty('access-control-allow-methods', 'POST'); + expect(res._getHeaders()).toHaveProperty('access-control-allow-headers', 'Content-Type'); + }); }); diff --git a/frontend/src/utils/pgSchemaTypeGen.js b/frontend/src/utils/pgSchemaTypeGen.js index a53df000..d4e4a4fb 100644 --- a/frontend/src/utils/pgSchemaTypeGen.js +++ b/frontend/src/utils/pgSchemaTypeGen.js @@ -103,9 +103,7 @@ export class PgSchemaTypeGen { Object.prototype.hasOwnProperty.call(columnSpec, 'definition') ) { this.addColumn(columnSpec, columns); - } else if ( - columnSpec.constraint_type === 'primary key' - ) { + } else if (columnSpec.constraint_type === 'primary key') { for (const foreignKeyDef of columnSpec.definition) { columns[foreignKeyDef.column.expr.value].nullable = false; } @@ -211,7 +209,7 @@ export class PgSchemaTypeGen { const tsType = columnDetails.nullable ? columnDetails.type + ' | null' : columnDetails.type; const optional = columnDetails.required ? '' : '?'; - queryDefinition += ` ${columnName}?: ${tsType} | ${tsType}[];\n` + queryDefinition += ` ${columnName}?: ${tsType} | ${tsType}[];\n`; itemDefinition += ` ${columnName}?: ${tsType};\n`; inputDefinition += ` ${columnName}${optional}: ${tsType};\n`; }