diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index d3f4c449..72ea96a9 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -15,6 +15,7 @@
"@near-lake/primitives": "0.5.0",
"bootstrap": "^5.2.3",
"buffer": "^6.0.3",
+ "genson-js": "^0.0.8",
"graphiql": "3.0.6",
"graphql": "^16.8.1",
"graphql-ws": "^5.16.0",
@@ -7146,6 +7147,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/genson-js": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/genson-js/-/genson-js-0.0.8.tgz",
+ "integrity": "sha512-4NUusDTwF+lzYh72uKV+Uvpky9iPO+YDIMpGImA5pbHfLV9HwgRCA4hYjGu78V4J4Cx2IZRTFfRERn9aUs74mw==",
+ "license": "Apache-2.0"
+ },
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 98e5c4e1..c2480305 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -26,6 +26,7 @@
"@near-lake/primitives": "0.5.0",
"bootstrap": "^5.2.3",
"buffer": "^6.0.3",
+ "genson-js": "^0.0.8",
"graphiql": "3.0.6",
"graphql": "^16.8.1",
"graphql-ws": "^5.16.0",
diff --git a/frontend/src/components/Editor/EditorComponents/GenerateCode.jsx b/frontend/src/components/Editor/EditorComponents/GenerateCode.jsx
deleted file mode 100644
index dedc6f63..00000000
--- a/frontend/src/components/Editor/EditorComponents/GenerateCode.jsx
+++ /dev/null
@@ -1,72 +0,0 @@
-//Dummy Component to generate code accessable accountId=test and indexerName=test and
-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 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}
-
-
-
- );
-};
-
-export default GenerateCode;
diff --git a/frontend/src/components/Editor/EditorComponents/GenerateCode.tsx b/frontend/src/components/Editor/EditorComponents/GenerateCode.tsx
new file mode 100644
index 00000000..bf41fedb
--- /dev/null
+++ b/frontend/src/components/Editor/EditorComponents/GenerateCode.tsx
@@ -0,0 +1,164 @@
+//Dummy component
+import { useState } from 'react';
+
+type Schema = {
+ type: string;
+ properties?: Record;
+ required?: string[];
+};
+
+type Method = {
+ method_name: string;
+ schema: Schema;
+};
+
+type Event = {
+ event_name: string;
+ schema: Schema;
+};
+
+const GenerateCode = () => {
+ const [contractFilter, setContractFilter] = useState('');
+ const [selectedMethods, setSelectedMethods] = useState([]);
+ const [selectedEvents, setSelectedEvents] = useState([]);
+ const [generatedCode, setGeneratedCode] = useState<{ jsCode: string; sqlCode: string }>({ 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 handleMethodChange = (index: number, field: keyof Method, value: string) => {
+ const updatedMethods = [...selectedMethods];
+ if (field === 'schema') {
+ try {
+ updatedMethods[index] = { ...updatedMethods[index], schema: JSON.parse(value) };
+ } catch (e) {
+ console.error('Invalid JSON format');
+ }
+ } else {
+ updatedMethods[index] = { ...updatedMethods[index], [field]: value };
+ }
+ setSelectedMethods(updatedMethods);
+ };
+
+ const handleEventChange = (index: number, field: keyof Event, value: string) => {
+ const updatedEvents = [...selectedEvents];
+ if (field === 'schema') {
+ try {
+ updatedEvents[index] = { ...updatedEvents[index], schema: JSON.parse(value) };
+ } catch (e) {
+ console.error('Invalid JSON format');
+ }
+ } else {
+ updatedEvents[index] = { ...updatedEvents[index], [field]: value };
+ }
+ setSelectedEvents(updatedEvents);
+ };
+
+ const addMethod = () => {
+ setSelectedMethods([...selectedMethods, { method_name: '', schema: { type: 'object' } }]);
+ };
+
+ const addEvent = () => {
+ setSelectedEvents([...selectedEvents, { event_name: '', schema: { type: 'object' } }]);
+ };
+
+ return (
+
+
+
Generate Code
+
+
+ setContractFilter(e.target.value)}
+ className="w-full p-3 border border-gray-300 rounded-md"
+ />
+
+
+
+
Selected Methods
+ {selectedMethods.map((method, index) => (
+
+ handleMethodChange(index, 'method_name', e.target.value)}
+ className="w-full p-3 border border-gray-300 rounded-md mb-2"
+ />
+
+ ))}
+
+
+
+
+
Selected Events
+ {selectedEvents.map((event, index) => (
+
+ handleEventChange(index, 'event_name', e.target.value)}
+ className="w-full p-3 border border-gray-300 rounded-md mb-2"
+ />
+
+ ))}
+
+
+
+
+
+
+
Generated JavaScript Code
+
{generatedCode.jsCode}
+
+
+
+
Generated SQL Code
+
{generatedCode.sqlCode}
+
+
+
+ );
+};
+
+export default GenerateCode;
diff --git a/frontend/src/pages/api/generateCode.ts b/frontend/src/pages/api/generateCode.ts
index 50c0d8bd..c5251894 100644
--- a/frontend/src/pages/api/generateCode.ts
+++ b/frontend/src/pages/api/generateCode.ts
@@ -1,44 +1,85 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { defaultCode, defaultSchema } from '../../utils/formatters';
+import { createSchema } from 'genson-js';
+import type { Schema } from 'genson-js/dist/types';
-interface RequestBody {
+export type Method = {
+ method_name: string;
+ schema: Schema;
+};
+
+export type Event = {
+ event_name: string;
+ schema: Schema;
+};
+
+export interface RequestBody {
contractFilter: string | string[];
- selectedMethods: string[];
- selectedEvents: string[];
+ selectedMethods: Method[];
+ selectedEvents: Event[];
}
-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'));
+export const isStringOrArray = (value: any): value is string | string[] =>
+ (typeof value === 'string' && value !== '') ||
+ (Array.isArray(value) && value.every((item) => typeof item === 'string'));
+
+export const isValidSchema = (schema: any): boolean => {
+ try {
+ createSchema(schema);
+ return true;
+ } catch {
+ return false;
+ }
+};
+export const validateRequestBody = (body: any): body is RequestBody => {
return (
isStringOrArray(body.contractFilter) &&
Array.isArray(body.selectedMethods) &&
- body.selectedMethods.every((method: any) => typeof method === 'string') &&
+ body.selectedMethods.every(isValidMethod) &&
Array.isArray(body.selectedEvents) &&
- body.selectedEvents.every((event: any) => typeof event === 'string')
+ body.selectedEvents.every(isValidEvent)
);
};
+export const isValidMethod = (item: any): item is Method =>
+ typeof item === 'object' &&
+ typeof item.method_name === 'string' &&
+ item.method_name.trim() !== '' &&
+ isValidSchema(item.schema);
+
+export const isValidEvent = (item: any): item is Event =>
+ typeof item === 'object' &&
+ typeof item.event_name === 'string' &&
+ item.event_name.trim() !== '' &&
+ isValidSchema(item.schema);
+
const generateDummyJSCode = (
contractFilter: string | string[],
- selectedMethods: string[],
- selectedEvents: string[],
+ selectedMethods: Method[],
+ selectedEvents: Event[],
): string => {
+ // All Types Of Methods
+ // const allMethodTypeList = selectedMethods.map(method => {
+ // return createSchema(method.schema);
+ // });
+
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`;
+ `-- Selected Methods: ${selectedMethods.map((m) => m.method_name).join(', ')}\n\n` +
+ `-- Selected Events: ${selectedEvents.map((e) => e.event_name).join(', ')}\n\n`;
const methodsJS = selectedMethods
- .map((method) => `function ${method}() {\n console.log('Executing ${method}');\n}\n\n`)
+ .map((method) => `function ${method.method_name}() {\n console.log('Executing ${method.method_name}');\n}\n\n`)
.join('');
const eventsJS = selectedEvents
- .map((event) => `function handle${event}() {\n console.log('Handling event ${event}');\n}\n\n`)
+ .map(
+ (event) => `function handle${event.event_name}() {\n console.log('Handling event ${event.event_name}');\n}\n\n`,
+ )
.join('');
return jsCodeHeader + defaultCode + methodsJS + eventsJS;
@@ -46,22 +87,29 @@ const generateDummyJSCode = (
const generateDummySQLCode = (
contractFilter: string | string[],
- selectedMethods: string[],
- selectedEvents: string[],
+ selectedMethods: Method[],
+ selectedEvents: Event[],
): string => {
+ // All Types Of Methods
+ // const allMethodTypeList = selectedMethods.map(method => {
+ // return createSchema(method.schema);
+ // });
+
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`;
+ `-- Selected Methods: ${selectedMethods.map((m) => m.method_name).join(', ')}\n\n` +
+ `-- Selected Events: ${selectedEvents.map((e) => e.event_name).join(', ')}\n\n`;
const methodsSQL = selectedMethods
- .map((method) => `-- Method: ${method}\nINSERT INTO methods (name) VALUES ('${method}');\n\n`)
+ .map(
+ (method) => `-- Method: ${method.method_name}\nINSERT INTO methods (name) VALUES ('${method.method_name}');\n\n`,
+ )
.join('');
const eventsSQL = selectedEvents
- .map((event) => `-- Event: ${event}\nINSERT INTO events (name) VALUES ('${event}');\n\n`)
+ .map((event) => `-- Event: ${event.event_name}\nINSERT INTO events (name) VALUES ('${event.event_name}');\n\n`)
.join('');
return sqlCodeHeader + defaultSchema + methodsSQL + eventsSQL;
@@ -69,13 +117,14 @@ const generateDummySQLCode = (
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-Methods', 'POST, OPTIONS');
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;
@@ -83,8 +132,7 @@ export default function handler(req: NextApiRequest, res: NextApiResponse): void
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',
+ error: 'Invalid request body: selectedMethods and selectedEvents must be arrays of objects with correct shape',
});
return;
}
diff --git a/frontend/src/test/api/generateCode.test.js b/frontend/src/test/api/generateCode.test.js
deleted file mode 100644
index 4fa52f28..00000000
--- a/frontend/src/test/api/generateCode.test.js
+++ /dev/null
@@ -1,189 +0,0 @@
-import { createMocks } from 'node-mocks-http';
-
-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'],
- },
- });
-
- 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);
-
- 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');
- });
-
- it('should return 400 for invalid contractFilter data type', async () => {
- const { req, res } = createMocks({
- method: 'POST',
- body: {
- contractFilter: 123,
- selectedMethods: ['method1'],
- selectedEvents: ['event1'],
- },
- });
-
- 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 for invalid method or event data types', async () => {
- const { req, res } = createMocks({
- method: 'POST',
- body: {
- contractFilter: 'filter',
- selectedMethods: [123],
- selectedEvents: ['event1'],
- },
- });
-
- 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 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,
- },
- });
-
- 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 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/test/api/generateCode.test.ts b/frontend/src/test/api/generateCode.test.ts
new file mode 100644
index 00000000..9f0422d8
--- /dev/null
+++ b/frontend/src/test/api/generateCode.test.ts
@@ -0,0 +1,140 @@
+import type { NextApiRequest, NextApiResponse } from 'next';
+import type { MockRequest, MockResponse } from 'node-mocks-http';
+import { createMocks } from 'node-mocks-http';
+
+import handler from '../../pages/api/generateCode';
+
+type CustomNextApiRequest = NextApiRequest & MockRequest;
+type CustomNextApiResponse = NextApiResponse & MockResponse;
+
+type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS';
+
+const createRequestResponseMocks = (
+ method: HttpMethod,
+ body: any,
+): { req: CustomNextApiRequest; res: CustomNextApiResponse } => {
+ const { req, res } = createMocks({
+ method,
+ body,
+ });
+ return {
+ req: req as CustomNextApiRequest,
+ res: res as CustomNextApiResponse,
+ };
+};
+
+it('should return generated JS and SQL code for valid input', async () => {
+ const { req, res } = createRequestResponseMocks('POST', {
+ contractFilter: 'filter',
+ selectedMethods: [
+ {
+ method_name: 'method1',
+ schema: { type: 'object' },
+ },
+ {
+ method_name: 'method2',
+ schema: { type: 'string' },
+ },
+ ],
+ selectedEvents: [
+ {
+ event_name: 'event1',
+ schema: { type: 'array', items: { type: 'string' } },
+ },
+ {
+ event_name: 'event2',
+ schema: { type: 'number' },
+ },
+ ],
+ });
+
+ handler(req, res);
+
+ expect(res._getStatusCode()).toBe(200);
+ const responseData = JSON.parse(res._getData());
+});
+it('should handle empty arrays correctly because I mean maybe they just want something to do with contractName?', async () => {
+ const { req, res } = createRequestResponseMocks('POST', {
+ contractFilter: 'filter',
+ selectedMethods: [],
+ selectedEvents: [],
+ });
+
+ handler(req, res);
+
+ expect(res._getStatusCode()).toBe(200);
+});
+
+it('should return 400 for invalid contractFilter data type', async () => {
+ const { req, res } = createRequestResponseMocks('POST', {
+ contractFilter: 123,
+ selectedMethods: [
+ {
+ method_name: 'method1',
+ schema: { type: 'object' },
+ },
+ ],
+ selectedEvents: [
+ {
+ event_name: 'event1',
+ schema: { type: 'string' },
+ },
+ ],
+ });
+
+ handler(req, res);
+
+ expect(res._getStatusCode()).toBe(400);
+ expect(JSON.parse(res._getData())).toEqual({
+ error: 'Invalid request body: selectedMethods and selectedEvents must be arrays of objects with correct shape',
+ });
+});
+
+it('should return 400 for missing contractFilter data type', async () => {
+ const { req, res } = createRequestResponseMocks('POST', {
+ contractFilter: '',
+ selectedMethods: [
+ {
+ method_name: 'method1',
+ schema: { type: 'object' },
+ },
+ ],
+ selectedEvents: [
+ {
+ event_name: 'event1',
+ schema: { type: 'string' },
+ },
+ ],
+ });
+
+ handler(req, res);
+
+ expect(res._getStatusCode()).toBe(400);
+ expect(JSON.parse(res._getData())).toEqual({
+ error: 'Invalid request body: selectedMethods and selectedEvents must be arrays of objects with correct shape',
+ });
+});
+
+it('returns 405 for GET method', () => {
+ const { req, res } = createRequestResponseMocks('POST', {
+ contractFilter: 'filter',
+ selectedMethods: [
+ {
+ method_name: 'method1',
+ schema: { type: 'object' },
+ },
+ ],
+ selectedEvents: [
+ {
+ event_name: 'event1',
+ schema: { type: 'string' },
+ },
+ ],
+ });
+
+ handler(req, res);
+
+ expect(res.getHeaders()).toHaveProperty('access-control-allow-origin', '*');
+ expect(res.getHeaders()).toHaveProperty('access-control-allow-methods', 'POST, OPTIONS');
+ expect(res.getHeaders()).toHaveProperty('access-control-allow-headers', 'Content-Type');
+});