added prompter, theme for canvas api, added endpoint to delete existing routes
This commit is contained in:
parent
f75be32123
commit
a07d1c7d39
44
README.md
44
README.md
@ -166,4 +166,46 @@ The backend API is documented using Swagger/OpenAPI. After starting the backend
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
|
||||
## Technical Documentation: ChatGPT-Powered Endpoint Creation
|
||||
|
||||
### Overview
|
||||
Developers can leverage the ChatGPT modal in the Canvas Endpoints UI to create new Canvas API endpoints using natural language prompts. When a user enters a prompt like "Create a course endpoint for Canvas", the system uses ChatGPT to:
|
||||
|
||||
1. Interpret the intent and generate a JSON object with the required fields for the endpoint (name, method, path, description, etc.).
|
||||
2. Automatically submit this JSON to the backend endpoint creation API (`/api/v1/canvas-api/endpoints`).
|
||||
3. Refresh the endpoint list in the UI and display a success message.
|
||||
|
||||
### How it Works
|
||||
- **Prompt Handling:**
|
||||
- The frontend sends the user's prompt to `/api/v1/canvas-api/chatgpt/completions`.
|
||||
- ChatGPT is instructed to return only a JSON object suitable for the endpoint creation form.
|
||||
- **Auto-Creation:**
|
||||
- If the response is a valid endpoint JSON (with `name`, `method`, and `path`), the frontend posts it to `/api/v1/canvas-api/endpoints`.
|
||||
- The endpoint list is refreshed and a toast notification is shown.
|
||||
- **Fallback:**
|
||||
- If the response is not a valid endpoint JSON, it is displayed as a normal chat message.
|
||||
|
||||
### Example Prompt
|
||||
```
|
||||
Create a course endpoint for Canvas. Use the Canvas API docs to determine the correct path and required fields.
|
||||
```
|
||||
|
||||
### Example ChatGPT Response
|
||||
```
|
||||
{
|
||||
"name": "Create Course",
|
||||
"method": "POST",
|
||||
"path": "/courses",
|
||||
"description": "Creates a new course in Canvas."
|
||||
}
|
||||
```
|
||||
|
||||
### Developer Notes
|
||||
- The ChatGPT modal logic is in `frontend/src/components/CanvasEndpoints.tsx`.
|
||||
- The backend endpoint creation API is `/api/v1/canvas-api/endpoints`.
|
||||
- The system expects ChatGPT to return a JSON object with at least `name`, `method`, and `path`.
|
||||
- The endpoint list is auto-refreshed after creation.
|
||||
|
||||
---
|
@ -13,8 +13,14 @@ const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
const config: Options = {
|
||||
driver: PostgreSqlDriver,
|
||||
entities: ['./dist/src/apps/_app/entities/**/*.js'],
|
||||
entitiesTs: ['./src/apps/_app/entities/**/*.ts'],
|
||||
entities: [
|
||||
'./dist/src/apps/_app/entities/**/*.js',
|
||||
'./dist/src/apps/canvas-api/entities/**/*.js',
|
||||
],
|
||||
entitiesTs: [
|
||||
'./src/apps/_app/entities/**/*.ts',
|
||||
'./src/apps/canvas-api/entities/**/*.ts',
|
||||
],
|
||||
extensions: [Migrator],
|
||||
baseDir: process.cwd(),
|
||||
discovery: {
|
||||
|
70
src/apps/canvas-api/CanvasGraphqlApi.ts
Normal file
70
src/apps/canvas-api/CanvasGraphqlApi.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import axios, { AxiosRequestConfig, AxiosResponse, AxiosError, Method } from 'axios';
|
||||
import UnableToProceed from '../../shared/exceptions/UnableToProceed';
|
||||
import Unauthorized from '../../shared/exceptions/Unauthorized';
|
||||
|
||||
// Allowed HTTP methods for external Canvas API requests
|
||||
const ALLOWED_METHODS: Method[] = ['GET']; // Expand this list in the future as needed
|
||||
|
||||
class CanvasGraphqlApi {
|
||||
private static readonly baseURL: string = process.env.CANVAS_API_URL || 'https://canvas.instructure.com/api/graphql';
|
||||
private static readonly accessToken: string = process.env.CANVAS_API_KEY || '';
|
||||
|
||||
public static async doGraphqlRequest<T>(
|
||||
query: string,
|
||||
variables?: Record<string, any>,
|
||||
headersExtra?: Record<string, string>,
|
||||
method: Method = 'GET'
|
||||
): Promise<T> {
|
||||
if (!ALLOWED_METHODS.includes(method)) {
|
||||
throw new UnableToProceed(`HTTP method ${method} is not allowed for external Canvas API requests.`);
|
||||
}
|
||||
|
||||
const headers = {
|
||||
Authorization: `Bearer ${CanvasGraphqlApi.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
...headersExtra,
|
||||
};
|
||||
|
||||
const requestBody = {
|
||||
query,
|
||||
variables: variables || {},
|
||||
};
|
||||
|
||||
const config: AxiosRequestConfig = {
|
||||
method,
|
||||
url: CanvasGraphqlApi.baseURL,
|
||||
headers,
|
||||
data: requestBody,
|
||||
};
|
||||
|
||||
console.log(`Sending request to Canvas GraphQL: ${config.url}`);
|
||||
console.log('Request Headers:', headers);
|
||||
console.log('Request Body:', JSON.stringify(requestBody, null, 2));
|
||||
|
||||
try {
|
||||
const response: AxiosResponse<T> = await axios(config);
|
||||
console.log('Received response from Canvas:', JSON.stringify(response.data, null, 2));
|
||||
return response.data;
|
||||
} catch (error: unknown) {
|
||||
const axiosError = error as AxiosError;
|
||||
console.error(`Error from Canvas GraphQL request:`, axiosError);
|
||||
if (axiosError.response) {
|
||||
console.error('Response Data:', JSON.stringify(axiosError.response.data, null, 2));
|
||||
if (axiosError.response.status === 401) {
|
||||
throw new Unauthorized('Unauthorized access');
|
||||
}
|
||||
throw new UnableToProceed(
|
||||
`HTTP ${axiosError.response.status} error at Canvas GraphQL: ${JSON.stringify(axiosError.response.data, null, 2)}`
|
||||
);
|
||||
} else if (axiosError.request) {
|
||||
console.error('No response received from Canvas');
|
||||
throw new UnableToProceed('No response from server');
|
||||
} else {
|
||||
console.error('Unexpected error:', axiosError.message);
|
||||
throw new UnableToProceed(axiosError.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default CanvasGraphqlApi;
|
@ -1,10 +1,21 @@
|
||||
import { FastifyPluginAsync } from 'fastify';
|
||||
// import configRoutes from './routes/ConfigRoutes';
|
||||
import dummyGradesRoutes from './routes/DummyGradesRoutes';
|
||||
import endpointsRoutes from './routes/EndpointsRoutes';
|
||||
import { ChatGptController } from './http/controllers/ChatGptController';
|
||||
import { ChatGptService } from './middleware/chatgpt/ChatGptService';
|
||||
|
||||
const canvasRoutes: FastifyPluginAsync = async (app) => {
|
||||
// app.register(configRoutes, { prefix: '/' });
|
||||
app.register(dummyGradesRoutes, { prefix: '/' });
|
||||
app.register(endpointsRoutes, { prefix: '/' });
|
||||
|
||||
const chatGptService = new ChatGptService();
|
||||
const chatGptController = new ChatGptController(chatGptService);
|
||||
|
||||
app.post('/chatgpt/completions', async (req, reply) => {
|
||||
await chatGptController.handleChatGptRequest(req, reply);
|
||||
});
|
||||
};
|
||||
|
||||
export default canvasRoutes;
|
||||
|
21
src/apps/canvas-api/entities/CanvasApiEndpoint.ts
Normal file
21
src/apps/canvas-api/entities/CanvasApiEndpoint.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { Entity, Property, ManyToOne } from '@mikro-orm/core';
|
||||
import { BaseEntity } from '../../_app/entities/_BaseEntity';
|
||||
import { User } from '../../_app/entities/user/_User';
|
||||
|
||||
@Entity({ tableName: 'canvas_api_endpoints' })
|
||||
export class CanvasApiEndpoint extends BaseEntity {
|
||||
@Property()
|
||||
name!: string;
|
||||
|
||||
@Property()
|
||||
method!: string;
|
||||
|
||||
@Property()
|
||||
path!: string;
|
||||
|
||||
@Property({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@ManyToOne(() => User, { nullable: true })
|
||||
user?: User;
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { CanvasApiEndpoint } from '../../entities/CanvasApiEndpoint';
|
||||
|
||||
export class CanvasApiEndpointController {
|
||||
async getAll(request: FastifyRequest, reply: FastifyReply) {
|
||||
const endpoints = await request.em.find(CanvasApiEndpoint, {});
|
||||
return reply.send(endpoints);
|
||||
}
|
||||
|
||||
async create(request: FastifyRequest, reply: FastifyReply) {
|
||||
const { name, method, path, description, user } = request.body as any;
|
||||
if (!name || !method || !path) {
|
||||
return reply.status(400).send({ error: 'name, method, and path are required' });
|
||||
}
|
||||
const endpoint = new CanvasApiEndpoint();
|
||||
endpoint.name = name;
|
||||
endpoint.method = method;
|
||||
endpoint.path = path;
|
||||
endpoint.description = description;
|
||||
endpoint.user = user;
|
||||
await request.em.persistAndFlush(endpoint);
|
||||
return reply.send(endpoint);
|
||||
}
|
||||
|
||||
async delete(request: FastifyRequest, reply: FastifyReply) {
|
||||
const id = Number(request.params['id']);
|
||||
if (!id) return reply.status(400).send({ error: 'Missing id' });
|
||||
const endpoint = await request.em.findOne(CanvasApiEndpoint, { id });
|
||||
if (!endpoint) return reply.status(404).send({ error: 'Not found' });
|
||||
await request.em.removeAndFlush(endpoint);
|
||||
return reply.send({ success: true });
|
||||
}
|
||||
}
|
23
src/apps/canvas-api/http/controllers/ChatGptController.ts
Normal file
23
src/apps/canvas-api/http/controllers/ChatGptController.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { ChatGptService } from '../../middleware/chatgpt/ChatGptService';
|
||||
|
||||
export class ChatGptController {
|
||||
constructor(private chatGptService: ChatGptService) {}
|
||||
|
||||
async handleChatGptRequest(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
const response = await this.chatGptService.sendMessageToChatGpt(request.body as Record<string, unknown>, reply);
|
||||
if (!reply.sent) {
|
||||
reply.send(response);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in ChatGptController:', error);
|
||||
if (!reply.sent) {
|
||||
reply.status(500).send({
|
||||
responseText: '',
|
||||
errorMessage: error instanceof Error ? error.message : 'Internal server error'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
65
src/apps/canvas-api/middleware/chatgpt/ChatGptRequest.ts
Normal file
65
src/apps/canvas-api/middleware/chatgpt/ChatGptRequest.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import axios, { AxiosRequestConfig, Method, AxiosResponse, AxiosError } from 'axios';
|
||||
import UnableToProceed from '../../../../shared/exceptions/UnableToProceed';
|
||||
import Unauthorized from '../../../../shared/exceptions/Unauthorized';
|
||||
import { HttpMethod } from '../../../../plugins/shared/types';
|
||||
import { ChatGptRequestConfig } from '../../types/chatgpt';
|
||||
|
||||
class ChatGptRequest {
|
||||
private static readonly baseURL: string = 'https://api.openai.com/v1';
|
||||
private static readonly apiKey: string = process.env.CHATGPT_API_KEY || '';
|
||||
|
||||
public static async doRequest<T>(
|
||||
method: HttpMethod,
|
||||
endpoint: string,
|
||||
body?: { model: string; messages: { role: string; content: string }[] },
|
||||
headersExtra?: Record<string, string>
|
||||
): Promise<T> {
|
||||
const headers = {
|
||||
Authorization: `Bearer ${ChatGptRequest.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
...headersExtra,
|
||||
};
|
||||
|
||||
const requestBody = {
|
||||
model: 'gpt-4', // Default model
|
||||
...body,
|
||||
};
|
||||
|
||||
const config: AxiosRequestConfig = {
|
||||
method: method as Method,
|
||||
url: `${ChatGptRequest.baseURL}${endpoint}`,
|
||||
headers,
|
||||
data: requestBody,
|
||||
};
|
||||
|
||||
console.log(`Sending request to OpenAI: ${config.url}`);
|
||||
console.log('Request Headers:', headers);
|
||||
console.log('Request Body:', JSON.stringify(requestBody, null, 2));
|
||||
|
||||
try {
|
||||
const response: AxiosResponse<T> = await axios(config);
|
||||
console.log('Received response from OpenAI:', JSON.stringify(response.data, null, 2));
|
||||
return response.data;
|
||||
} catch (error: unknown) {
|
||||
const axiosError = error as AxiosError;
|
||||
console.error(`Error from OpenAI request to ${endpoint}:`, axiosError);
|
||||
if (axiosError.response) {
|
||||
console.error('Response Data:', JSON.stringify(axiosError.response.data, null, 2));
|
||||
if (axiosError.response.status === 401) {
|
||||
throw new Unauthorized('Unauthorized access');
|
||||
}
|
||||
throw new UnableToProceed(
|
||||
`HTTP ${axiosError.response.status} error at ${endpoint}: ${JSON.stringify(axiosError.response.data, null, 2)}`
|
||||
);
|
||||
} else if (axiosError.request) {
|
||||
console.error('No response received from OpenAI');
|
||||
throw new UnableToProceed('No response from server');
|
||||
} else {
|
||||
console.error('Unexpected error:', axiosError.message);
|
||||
throw new UnableToProceed(axiosError.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ChatGptRequest;
|
68
src/apps/canvas-api/middleware/chatgpt/ChatGptService.ts
Normal file
68
src/apps/canvas-api/middleware/chatgpt/ChatGptService.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { HttpMethod } from '../../../../plugins/shared/types';
|
||||
import ChatGptRequest from './ChatGptRequest';
|
||||
import { FastifyReply } from 'fastify';
|
||||
import { ChatGptResponse, OpenAIResponse, ChatGptRequestBody } from '../../types/chatgpt';
|
||||
|
||||
export class ChatGptService {
|
||||
private endpoint: string = '/chat/completions';
|
||||
|
||||
async sendMessageToChatGpt(requestBody: Record<string, unknown>, reply: FastifyReply): Promise<ChatGptResponse> {
|
||||
try {
|
||||
let messages: { role: string; content: string }[];
|
||||
let responseFormat: string | undefined;
|
||||
|
||||
if ('data' in requestBody && typeof requestBody.data === 'string') {
|
||||
messages = [
|
||||
{
|
||||
role: 'system',
|
||||
content: `You are an assistant for a developer tool. When the user asks for a Canvas API endpoint, respond ONLY with a JSON object in this format:\n{\n "name": "Get All Courses",\n "method": "GET",\n "path": "/courses",\n "description": "Retrieves all courses from Canvas"\n}\nDo not include any explanation, just the JSON. Use the official Canvas API documentation to determine the correct method and path. The path should be exactly as in the Canvas API documentation, e.g., '/courses', and should NOT include any base URL or prefix such as '/api/v1'.`
|
||||
},
|
||||
{ role: 'user', content: requestBody.data as string }
|
||||
];
|
||||
responseFormat = requestBody.responseFormat as string;
|
||||
} else if (
|
||||
'messages' in requestBody &&
|
||||
Array.isArray(requestBody.messages) &&
|
||||
requestBody.messages.length > 0
|
||||
) {
|
||||
messages = requestBody.messages as { role: string; content: string }[];
|
||||
responseFormat = requestBody.responseFormat as string;
|
||||
} else {
|
||||
throw new Error("Missing required parameter: 'messages' or 'data'");
|
||||
}
|
||||
|
||||
if (!('model' in requestBody) || typeof requestBody.model !== 'string') {
|
||||
requestBody.model = 'gpt-4';
|
||||
}
|
||||
|
||||
const formattedRequest: ChatGptRequestBody = {
|
||||
model: requestBody.model as string,
|
||||
messages,
|
||||
response_format: responseFormat === 'json' ? { type: 'json_object' } : undefined
|
||||
};
|
||||
|
||||
const response = await ChatGptRequest.doRequest<OpenAIResponse>("POST", this.endpoint, formattedRequest);
|
||||
|
||||
const responseText = response?.choices?.[0]?.message?.content ?? 'No response from ChatGPT';
|
||||
|
||||
let parsedResponse: string | object = responseText;
|
||||
if (responseFormat === 'json') {
|
||||
try {
|
||||
parsedResponse = JSON.parse(responseText);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse JSON response:', e);
|
||||
throw new Error('Invalid JSON response from ChatGPT');
|
||||
}
|
||||
}
|
||||
|
||||
return { responseText: parsedResponse };
|
||||
} catch (error) {
|
||||
console.error('Error handling ChatGPT request:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to process request';
|
||||
if (!reply.sent) {
|
||||
reply.status(500).send({ responseText: '', errorMessage });
|
||||
}
|
||||
return { responseText: '', errorMessage };
|
||||
}
|
||||
}
|
||||
}
|
56
src/apps/canvas-api/routes/EndpointsRoutes.ts
Normal file
56
src/apps/canvas-api/routes/EndpointsRoutes.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { FastifyPluginAsync, FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { CanvasApiEndpointController } from '../http/controllers/CanvasApiEndpointController';
|
||||
import axios, { Method } from 'axios';
|
||||
|
||||
const ALLOWED_METHODS: Method[] = ['GET']; // Expand as needed
|
||||
|
||||
// Type for proxy request input
|
||||
interface CanvasProxyRequestInput {
|
||||
path: string;
|
||||
method?: Method;
|
||||
params?: any;
|
||||
}
|
||||
|
||||
const endpointsRoutes: FastifyPluginAsync = async (app) => {
|
||||
const controller = new CanvasApiEndpointController();
|
||||
|
||||
app.get('/endpoints', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
return controller.getAll(request, reply);
|
||||
});
|
||||
|
||||
app.post('/endpoints', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
return controller.create(request, reply);
|
||||
});
|
||||
|
||||
app.delete('/endpoints/:id', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
return controller.delete(request, reply);
|
||||
});
|
||||
|
||||
// Dynamic proxy route for external Canvas REST API
|
||||
app.post('/proxy-external', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
try {
|
||||
let baseUrl = process.env.CANVAS_API_URL || 'https://talnet.instructure.com';
|
||||
// Remove trailing /api/v1 if present
|
||||
baseUrl = baseUrl.replace(/\/api\/v1$/, '');
|
||||
const apiKey = process.env.CANVAS_API_KEY || '';
|
||||
const { path, method = 'GET', params } = request.body as CanvasProxyRequestInput;
|
||||
if (!path) return reply.status(400).send({ error: 'Missing path' });
|
||||
if (!ALLOWED_METHODS.includes(method as Method)) {
|
||||
return reply.status(405).send({ error: `Method ${method} not allowed` });
|
||||
}
|
||||
const url = `${baseUrl}/api/v1${path}`;
|
||||
const response = await axios.request({
|
||||
url,
|
||||
method,
|
||||
headers: { Authorization: `Bearer ${apiKey}` },
|
||||
params: method === 'GET' ? params : undefined,
|
||||
data: method !== 'GET' ? params : undefined,
|
||||
});
|
||||
return reply.send(response.data);
|
||||
} catch (err) {
|
||||
return reply.status(500).send({ error: err.message });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default endpointsRoutes;
|
21
src/apps/canvas-api/types/chatgpt.ts
Normal file
21
src/apps/canvas-api/types/chatgpt.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export interface ChatGptResponse {
|
||||
responseText: string | object;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export interface OpenAIResponse {
|
||||
choices: { index: number; message: { role: string; content: string } }[];
|
||||
}
|
||||
|
||||
export interface ChatGptRequestBody {
|
||||
model: string;
|
||||
messages: { role: string; content: string }[];
|
||||
response_format?: { type: string };
|
||||
}
|
||||
|
||||
export interface ChatGptRequestConfig {
|
||||
method: string;
|
||||
url: string;
|
||||
headers: Record<string, string>;
|
||||
data: ChatGptRequestBody;
|
||||
}
|
757
src/database/migrations/.snapshot-fusero-boilerplate-db.json
Normal file
757
src/database/migrations/.snapshot-fusero-boilerplate-db.json
Normal file
@ -0,0 +1,757 @@
|
||||
{
|
||||
"namespaces": [
|
||||
"public"
|
||||
],
|
||||
"name": "public",
|
||||
"tables": [
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"unsigned": false,
|
||||
"autoincrement": true,
|
||||
"primary": true,
|
||||
"nullable": false,
|
||||
"mappedType": "integer"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 255,
|
||||
"mappedType": "string"
|
||||
}
|
||||
},
|
||||
"name": "app",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "app_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {},
|
||||
"nativeEnums": {}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"unsigned": false,
|
||||
"autoincrement": true,
|
||||
"primary": true,
|
||||
"nullable": false,
|
||||
"mappedType": "integer"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 255,
|
||||
"mappedType": "string"
|
||||
}
|
||||
},
|
||||
"name": "tenant",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "tenant_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {},
|
||||
"nativeEnums": {}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"unsigned": false,
|
||||
"autoincrement": true,
|
||||
"primary": true,
|
||||
"nullable": false,
|
||||
"mappedType": "integer"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "varchar(255)",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 255,
|
||||
"mappedType": "string"
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "varchar(255)",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 255,
|
||||
"mappedType": "string"
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(255)",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 255,
|
||||
"mappedType": "string"
|
||||
}
|
||||
},
|
||||
"name": "user",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"columnNames": [
|
||||
"username"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "user_username_unique",
|
||||
"constraint": true,
|
||||
"primary": false,
|
||||
"unique": true
|
||||
},
|
||||
{
|
||||
"keyName": "user_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {},
|
||||
"nativeEnums": {}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"unsigned": false,
|
||||
"autoincrement": true,
|
||||
"primary": true,
|
||||
"nullable": false,
|
||||
"mappedType": "integer"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"tenant_id": {
|
||||
"name": "tenant_id",
|
||||
"type": "int",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "integer"
|
||||
},
|
||||
"app_id": {
|
||||
"name": "app_id",
|
||||
"type": "int",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "integer"
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "int",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "integer"
|
||||
}
|
||||
},
|
||||
"name": "tenant_app",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "tenant_app_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {
|
||||
"tenant_app_tenant_id_foreign": {
|
||||
"constraintName": "tenant_app_tenant_id_foreign",
|
||||
"columnNames": [
|
||||
"tenant_id"
|
||||
],
|
||||
"localTableName": "public.tenant_app",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.tenant",
|
||||
"updateRule": "cascade"
|
||||
},
|
||||
"tenant_app_app_id_foreign": {
|
||||
"constraintName": "tenant_app_app_id_foreign",
|
||||
"columnNames": [
|
||||
"app_id"
|
||||
],
|
||||
"localTableName": "public.tenant_app",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.app",
|
||||
"updateRule": "cascade"
|
||||
},
|
||||
"tenant_app_user_id_foreign": {
|
||||
"constraintName": "tenant_app_user_id_foreign",
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"localTableName": "public.tenant_app",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.user",
|
||||
"updateRule": "cascade"
|
||||
}
|
||||
},
|
||||
"nativeEnums": {}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"unsigned": false,
|
||||
"autoincrement": true,
|
||||
"primary": true,
|
||||
"nullable": false,
|
||||
"mappedType": "integer"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "int",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "integer"
|
||||
},
|
||||
"app_id": {
|
||||
"name": "app_id",
|
||||
"type": "int",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "integer"
|
||||
},
|
||||
"encrypted_credentials": {
|
||||
"name": "encrypted_credentials",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "text"
|
||||
}
|
||||
},
|
||||
"name": "credential",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "credential_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {
|
||||
"credential_user_id_foreign": {
|
||||
"constraintName": "credential_user_id_foreign",
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"localTableName": "public.credential",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.user",
|
||||
"updateRule": "cascade"
|
||||
},
|
||||
"credential_app_id_foreign": {
|
||||
"constraintName": "credential_app_id_foreign",
|
||||
"columnNames": [
|
||||
"app_id"
|
||||
],
|
||||
"localTableName": "public.credential",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.app",
|
||||
"updateRule": "cascade"
|
||||
}
|
||||
},
|
||||
"nativeEnums": {}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"unsigned": false,
|
||||
"autoincrement": true,
|
||||
"primary": true,
|
||||
"nullable": false,
|
||||
"mappedType": "integer"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 255,
|
||||
"mappedType": "string"
|
||||
},
|
||||
"method": {
|
||||
"name": "method",
|
||||
"type": "varchar(255)",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 255,
|
||||
"mappedType": "string"
|
||||
},
|
||||
"path": {
|
||||
"name": "path",
|
||||
"type": "varchar(255)",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 255,
|
||||
"mappedType": "string"
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "varchar(255)",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"length": 255,
|
||||
"mappedType": "string"
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "int",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "integer"
|
||||
}
|
||||
},
|
||||
"name": "canvas_api_endpoints",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "canvas_api_endpoints_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {
|
||||
"canvas_api_endpoints_user_id_foreign": {
|
||||
"constraintName": "canvas_api_endpoints_user_id_foreign",
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"localTableName": "public.canvas_api_endpoints",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.user",
|
||||
"deleteRule": "set null",
|
||||
"updateRule": "cascade"
|
||||
}
|
||||
},
|
||||
"nativeEnums": {}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"unsigned": false,
|
||||
"autoincrement": true,
|
||||
"primary": true,
|
||||
"nullable": false,
|
||||
"mappedType": "integer"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "varchar(255)",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 255,
|
||||
"mappedType": "string"
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "int",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "integer"
|
||||
}
|
||||
},
|
||||
"name": "apikey",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "apikey_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {
|
||||
"apikey_user_id_foreign": {
|
||||
"constraintName": "apikey_user_id_foreign",
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"localTableName": "public.apikey",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.user",
|
||||
"updateRule": "cascade"
|
||||
}
|
||||
},
|
||||
"nativeEnums": {}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"unsigned": false,
|
||||
"autoincrement": true,
|
||||
"primary": true,
|
||||
"nullable": false,
|
||||
"mappedType": "integer"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"length": 255,
|
||||
"mappedType": "string"
|
||||
}
|
||||
},
|
||||
"name": "user_role",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "user_role_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"constraint": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {},
|
||||
"nativeEnums": {}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "int",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "integer"
|
||||
},
|
||||
"user_role_id": {
|
||||
"name": "user_role_id",
|
||||
"type": "int",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "integer"
|
||||
}
|
||||
},
|
||||
"name": "user_roles",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "user_roles_pkey",
|
||||
"columnNames": [
|
||||
"user_id",
|
||||
"user_role_id"
|
||||
],
|
||||
"composite": true,
|
||||
"constraint": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {
|
||||
"user_roles_user_id_foreign": {
|
||||
"constraintName": "user_roles_user_id_foreign",
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"localTableName": "public.user_roles",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.user",
|
||||
"deleteRule": "cascade",
|
||||
"updateRule": "cascade"
|
||||
},
|
||||
"user_roles_user_role_id_foreign": {
|
||||
"constraintName": "user_roles_user_role_id_foreign",
|
||||
"columnNames": [
|
||||
"user_role_id"
|
||||
],
|
||||
"localTableName": "public.user_roles",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.user_role",
|
||||
"deleteRule": "cascade",
|
||||
"updateRule": "cascade"
|
||||
}
|
||||
},
|
||||
"nativeEnums": {}
|
||||
}
|
||||
],
|
||||
"nativeEnums": {}
|
||||
}
|
15
src/database/migrations/Migration20250502161232.ts
Normal file
15
src/database/migrations/Migration20250502161232.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class Migration20250502161232 extends Migration {
|
||||
|
||||
override async up(): Promise<void> {
|
||||
this.addSql(`create table "canvas_api_endpoints" ("id" serial primary key, "created_at" timestamptz not null, "updated_at" timestamptz not null, "name" varchar(255) not null, "method" varchar(255) not null, "path" varchar(255) not null, "description" varchar(255) null, "user_id" int null);`);
|
||||
|
||||
this.addSql(`alter table "canvas_api_endpoints" add constraint "canvas_api_endpoints_user_id_foreign" foreign key ("user_id") references "user" ("id") on update cascade on delete set null;`);
|
||||
}
|
||||
|
||||
override async down(): Promise<void> {
|
||||
this.addSql(`drop table if exists "canvas_api_endpoints" cascade;`);
|
||||
}
|
||||
|
||||
}
|
13
src/database/migrations/Migration20250505072850.ts
Normal file
13
src/database/migrations/Migration20250505072850.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class Migration20250505072850 extends Migration {
|
||||
|
||||
override async up(): Promise<void> {
|
||||
this.addSql(`alter table "canvas_api_endpoints" drop column "dummy";`);
|
||||
}
|
||||
|
||||
override async down(): Promise<void> {
|
||||
this.addSql(`alter table "canvas_api_endpoints" add column "dummy" varchar(255) null;`);
|
||||
}
|
||||
|
||||
}
|
@ -1,9 +1 @@
|
||||
export enum HttpMethod {
|
||||
GET = 'GET',
|
||||
POST = 'POST',
|
||||
PUT = 'PUT',
|
||||
PATCH = 'PATCH',
|
||||
DELETE = 'DELETE',
|
||||
HEAD = 'HEAD',
|
||||
OPTIONS = 'OPTIONS',
|
||||
}
|
||||
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
||||
|
@ -4,6 +4,7 @@ class UnableToProceed extends Error {
|
||||
public httpErrorStatusCode = HttpStatusCode.BadRequest;
|
||||
constructor(msg: string, customErrorCode?: typeof HttpStatusCode) {
|
||||
super(msg);
|
||||
this.name = 'UnableToProceed';
|
||||
|
||||
this.httpErrorStatusCode = this.httpErrorStatusCode ?? customErrorCode;
|
||||
|
||||
|
@ -2,8 +2,9 @@ import { HttpStatusCode } from 'axios';
|
||||
|
||||
class Unauthorized extends Error {
|
||||
public httpErrorStatusCode = HttpStatusCode.Unauthorized;
|
||||
constructor(msg: string = 'Unauthorized') {
|
||||
super(msg);
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'Unauthorized';
|
||||
// Set the prototype explicitly.
|
||||
Object.setPrototypeOf(this, Unauthorized.prototype);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user