diff --git a/docs/openapi.json b/docs/openapi.json index 769eca7891..e8167b1d23 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -118398,70 +118398,182 @@ "Roles" ], "description": "Get all roles in the organization", + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "search", + "required": false + }, + { + "schema": { + "default": 20, + "type": "integer", + "minimum": 1, + "maximum": 100 + }, + "in": "query", + "name": "limit", + "required": false + }, + { + "schema": { + "default": 0, + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "in": "query", + "name": "offset", + "required": false + }, + { + "schema": { + "type": "string", + "enum": [ + "name", + "createdAt" + ] + }, + "in": "query", + "name": "sortBy", + "required": false + }, + { + "schema": { + "default": "desc", + "type": "string", + "enum": [ + "asc", + "desc" + ] + }, + "in": "query", + "name": "sortDirection", + "required": false + } + ], "responses": { "200": { "description": "Default Response", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "organizationId": { - "type": "string" - }, - "role": { - "type": "string" - }, - "name": { - "type": "string" - }, - "permission": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { "type": "object", - "additionalProperties": { - "type": "array", - "items": { + "properties": { + "id": { + "type": "string" + }, + "organizationId": { + "type": "string" + }, + "role": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "nullable": true, + "type": "string" + }, + "permission": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "create", + "read", + "update", + "delete", + "team-admin", + "admin", + "cancel" + ] + } + } + }, + "createdAt": { "type": "string", - "enum": [ - "create", - "read", - "update", - "delete", - "team-admin", - "admin", - "cancel" - ] + "format": "date-time" + }, + "updatedAt": { + "nullable": true, + "type": "string", + "format": "date-time" + }, + "predefined": { + "type": "boolean" } - } - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "nullable": true, - "type": "string", - "format": "date-time" - }, - "predefined": { - "type": "boolean" + }, + "required": [ + "id", + "role", + "name", + "description", + "permission", + "createdAt", + "updatedAt", + "predefined" + ], + "additionalProperties": false } }, - "required": [ - "id", - "role", - "name", - "permission", - "createdAt", - "updatedAt", - "predefined" - ], - "additionalProperties": false - } + "pagination": { + "type": "object", + "properties": { + "currentPage": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "total": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "totalPages": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + }, + "required": [ + "currentPage", + "limit", + "total", + "totalPages", + "hasNext", + "hasPrev" + ], + "additionalProperties": false + } + }, + "required": [ + "data", + "pagination" + ], + "additionalProperties": false } } } @@ -118695,6 +118807,11 @@ "minLength": 1, "maxLength": 50 }, + "description": { + "nullable": true, + "type": "string", + "maxLength": 500 + }, "permission": { "type": "object", "additionalProperties": { @@ -118743,6 +118860,10 @@ "name": { "type": "string" }, + "description": { + "nullable": true, + "type": "string" + }, "permission": { "type": "object", "additionalProperties": { @@ -118778,6 +118899,7 @@ "id", "role", "name", + "description", "permission", "createdAt", "updatedAt", @@ -119053,6 +119175,10 @@ "name": { "type": "string" }, + "description": { + "nullable": true, + "type": "string" + }, "permission": { "type": "object", "additionalProperties": { @@ -119088,6 +119214,7 @@ "id", "role", "name", + "description", "permission", "createdAt", "updatedAt", @@ -119327,6 +119454,11 @@ "minLength": 1, "maxLength": 50 }, + "description": { + "nullable": true, + "type": "string", + "maxLength": 500 + }, "permission": { "type": "object", "additionalProperties": { @@ -119395,6 +119527,10 @@ "name": { "type": "string" }, + "description": { + "nullable": true, + "type": "string" + }, "permission": { "type": "object", "additionalProperties": { @@ -119430,6 +119566,7 @@ "id", "role", "name", + "description", "permission", "createdAt", "updatedAt", @@ -123631,50 +123768,464 @@ } } }, - "/v1/perplexity/chat/completions": { - "post": { - "operationId": "perplexityChatCompletionsWithDefaultAgent", + "/api/organization/members/paginated": { + "get": { + "operationId": "getOrganizationMembersPaginated", "tags": [ - "llm-proxy" + "Organization" ], - "description": "Create a chat completion with Perplexity (uses default agent). Note: Perplexity does not support external tool calling.", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/XaiChatCompletionRequestInput" - } - } - } - }, + "description": "Get paginated members of the organization with search, sorting, and filtering", "parameters": [ { "schema": { "type": "string" }, - "in": "header", - "name": "user-agent", - "required": false, - "description": "The user agent of the client" + "in": "query", + "name": "search", + "required": false }, { "schema": { "type": "string" }, - "in": "header", - "name": "authorization", - "required": true, - "description": "Bearer token for OpenAI" - } - ], - "responses": { - "200": { - "description": "Default Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PerplexityChatCompletionResponse" - } + "in": "query", + "name": "teamIds", + "required": false + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "role", + "required": false + }, + { + "schema": { + "default": 20, + "type": "integer", + "minimum": 1, + "maximum": 100 + }, + "in": "query", + "name": "limit", + "required": false + }, + { + "schema": { + "default": 0, + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "in": "query", + "name": "offset", + "required": false + }, + { + "schema": { + "type": "string", + "enum": [ + "name", + "email", + "role", + "createdAt" + ] + }, + "in": "query", + "name": "sortBy", + "required": false + }, + { + "schema": { + "default": "desc", + "type": "string", + "enum": [ + "asc", + "desc" + ] + }, + "in": "query", + "name": "sortDirection", + "required": false + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "userId": { + "type": "string" + }, + "name": { + "nullable": true, + "type": "string" + }, + "email": { + "type": "string" + }, + "role": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "teams": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ], + "additionalProperties": false + } + }, + "isPendingSignup": { + "type": "boolean" + } + }, + "required": [ + "id", + "userId", + "name", + "email", + "role", + "createdAt", + "teams", + "isPendingSignup" + ], + "additionalProperties": false + } + }, + "pagination": { + "type": "object", + "properties": { + "currentPage": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "total": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "totalPages": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + }, + "required": [ + "currentPage", + "limit", + "total", + "totalPages", + "hasNext", + "hasPrev" + ], + "additionalProperties": false + } + }, + "required": [ + "data", + "pagination" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "api_validation_error" + ] + } + }, + "required": [ + "message", + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "error" + ], + "additionalProperties": false + } + } + } + }, + "401": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "api_authentication_error" + ] + } + }, + "required": [ + "message", + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "error" + ], + "additionalProperties": false + } + } + } + }, + "403": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "api_authorization_error" + ] + } + }, + "required": [ + "message", + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "error" + ], + "additionalProperties": false + } + } + } + }, + "404": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "api_not_found_error" + ] + } + }, + "required": [ + "message", + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "error" + ], + "additionalProperties": false + } + } + } + }, + "409": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "api_conflict_error" + ] + } + }, + "required": [ + "message", + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "error" + ], + "additionalProperties": false + } + } + } + }, + "500": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "api_internal_server_error" + ] + } + }, + "required": [ + "message", + "type" + ], + "additionalProperties": false + } + }, + "required": [ + "error" + ], + "additionalProperties": false + } + } + } + } + } + } + }, + "/v1/perplexity/chat/completions": { + "post": { + "operationId": "perplexityChatCompletionsWithDefaultAgent", + "tags": [ + "llm-proxy" + ], + "description": "Create a chat completion with Perplexity (uses default agent). Note: Perplexity does not support external tool calling.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/XaiChatCompletionRequestInput" + } + } + } + }, + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "header", + "name": "user-agent", + "required": false, + "description": "The user agent of the client" + }, + { + "schema": { + "type": "string" + }, + "in": "header", + "name": "authorization", + "required": true, + "description": "Bearer token for OpenAI" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PerplexityChatCompletionResponse" + } } } }, @@ -126515,92 +127066,200 @@ "Teams" ], "description": "Get all teams in the organization", + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "search", + "required": false + }, + { + "schema": { + "default": 20, + "type": "integer", + "minimum": 1, + "maximum": 100 + }, + "in": "query", + "name": "limit", + "required": false + }, + { + "schema": { + "default": 0, + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "in": "query", + "name": "offset", + "required": false + }, + { + "schema": { + "type": "string", + "enum": [ + "name", + "createdAt", + "memberCount" + ] + }, + "in": "query", + "name": "sortBy", + "required": false + }, + { + "schema": { + "default": "desc", + "type": "string", + "enum": [ + "asc", + "desc" + ] + }, + "in": "query", + "name": "sortDirection", + "required": false + } + ], "responses": { "200": { "description": "Default Response", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "description": { - "nullable": true, - "type": "string" - }, - "organizationId": { - "type": "string" - }, - "createdBy": { - "type": "string" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - }, - "convertToolResultsToToon": { - "type": "boolean" - }, - "members": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "teamId": { - "type": "string" - }, - "userId": { - "type": "string" - }, - "role": { - "type": "string" - }, - "syncedFromSso": { - "type": "boolean" - }, - "createdAt": { - "type": "string", - "format": "date-time" - } + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" }, - "required": [ - "id", - "teamId", - "userId", - "role", - "syncedFromSso", - "createdAt" - ], - "additionalProperties": false - } + "name": { + "type": "string" + }, + "description": { + "nullable": true, + "type": "string" + }, + "organizationId": { + "type": "string" + }, + "createdBy": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "convertToolResultsToToon": { + "type": "boolean" + }, + "members": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "teamId": { + "type": "string" + }, + "userId": { + "type": "string" + }, + "role": { + "type": "string" + }, + "syncedFromSso": { + "type": "boolean" + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "teamId", + "userId", + "role", + "syncedFromSso", + "createdAt" + ], + "additionalProperties": false + } + } + }, + "required": [ + "id", + "name", + "description", + "organizationId", + "createdBy", + "createdAt", + "updatedAt", + "convertToolResultsToToon" + ], + "additionalProperties": false } }, - "required": [ - "id", - "name", - "description", - "organizationId", - "createdBy", - "createdAt", - "updatedAt", - "convertToolResultsToToon" - ], - "additionalProperties": false - } + "pagination": { + "type": "object", + "properties": { + "currentPage": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "total": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "totalPages": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + }, + "required": [ + "currentPage", + "limit", + "total", + "totalPages", + "hasNext", + "hasPrev" + ], + "additionalProperties": false + } + }, + "required": [ + "data", + "pagination" + ], + "additionalProperties": false } } } diff --git a/platform/backend/src/auth/better-auth.ts b/platform/backend/src/auth/better-auth.ts index 44589e7981..a66e30e649 100644 --- a/platform/backend/src/auth/better-auth.ts +++ b/platform/backend/src/auth/better-auth.ts @@ -110,6 +110,10 @@ export const auth: any = betterAuth({ type: "string", required: true, }, + description: { + type: "string", + required: false, + }, }, }, }, diff --git a/platform/backend/src/database/migrations/0175_colossal_aaron_stack.sql b/platform/backend/src/database/migrations/0175_colossal_aaron_stack.sql new file mode 100644 index 0000000000..d6b45ae6da --- /dev/null +++ b/platform/backend/src/database/migrations/0175_colossal_aaron_stack.sql @@ -0,0 +1 @@ +ALTER TABLE "organization_role" ADD COLUMN "description" text; \ No newline at end of file diff --git a/platform/backend/src/database/migrations/meta/0175_snapshot.json b/platform/backend/src/database/migrations/meta/0175_snapshot.json new file mode 100644 index 0000000000..ceaad6603a --- /dev/null +++ b/platform/backend/src/database/migrations/meta/0175_snapshot.json @@ -0,0 +1,7622 @@ +{ + "id": "dc836c72-2515-4715-8e93-62b282f9805d", + "prevId": "c792aa2c-4c0e-41b2-a320-fa5b44e4bc6d", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_connector_assignment": { + "name": "agent_connector_assignment", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_connector_assignment_agent_idx": { + "name": "agent_connector_assignment_agent_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_connector_assignment_connector_idx": { + "name": "agent_connector_assignment_connector_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_connector_assignment_agent_id_agents_id_fk": { + "name": "agent_connector_assignment_agent_id_agents_id_fk", + "tableFrom": "agent_connector_assignment", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_connector_assignment_connector_id_knowledge_base_connectors_id_fk": { + "name": "agent_connector_assignment_connector_id_knowledge_base_connectors_id_fk", + "tableFrom": "agent_connector_assignment", + "tableTo": "knowledge_base_connectors", + "columnsFrom": [ + "connector_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "agent_connector_assignment_agent_id_connector_id_pk": { + "name": "agent_connector_assignment_agent_id_connector_id_pk", + "columns": [ + "agent_id", + "connector_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_knowledge_base": { + "name": "agent_knowledge_base", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_knowledge_base_agent_idx": { + "name": "agent_knowledge_base_agent_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_knowledge_base_kb_idx": { + "name": "agent_knowledge_base_kb_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_knowledge_base_agent_id_agents_id_fk": { + "name": "agent_knowledge_base_agent_id_agents_id_fk", + "tableFrom": "agent_knowledge_base", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_knowledge_base_knowledge_base_id_knowledge_bases_id_fk": { + "name": "agent_knowledge_base_knowledge_base_id_knowledge_bases_id_fk", + "tableFrom": "agent_knowledge_base", + "tableTo": "knowledge_bases", + "columnsFrom": [ + "knowledge_base_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "agent_knowledge_base_agent_id_knowledge_base_id_pk": { + "name": "agent_knowledge_base_agent_id_knowledge_base_id_pk", + "columns": [ + "agent_id", + "knowledge_base_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_labels": { + "name": "agent_labels", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "value_id": { + "name": "value_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "agent_labels_agent_id_agents_id_fk": { + "name": "agent_labels_agent_id_agents_id_fk", + "tableFrom": "agent_labels", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_labels_key_id_label_keys_id_fk": { + "name": "agent_labels_key_id_label_keys_id_fk", + "tableFrom": "agent_labels", + "tableTo": "label_keys", + "columnsFrom": [ + "key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_labels_value_id_label_values_id_fk": { + "name": "agent_labels_value_id_label_values_id_fk", + "tableFrom": "agent_labels", + "tableTo": "label_values", + "columnsFrom": [ + "value_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "agent_labels_agent_id_key_id_pk": { + "name": "agent_labels_agent_id_key_id_pk", + "columns": [ + "agent_id", + "key_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_team": { + "name": "agent_team", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "agent_team_agent_id_agents_id_fk": { + "name": "agent_team_agent_id_agents_id_fk", + "tableFrom": "agent_team", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_team_team_id_team_id_fk": { + "name": "agent_team_team_id_team_id_fk", + "tableFrom": "agent_team", + "tableTo": "team", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "agent_team_agent_id_team_id_pk": { + "name": "agent_team_agent_id_team_id_pk", + "columns": [ + "agent_id", + "team_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_tools": { + "name": "agent_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tool_id": { + "name": "tool_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "response_modifier_template": { + "name": "response_modifier_template", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_source_mcp_server_id": { + "name": "credential_source_mcp_server_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_source_mcp_server_id": { + "name": "execution_source_mcp_server_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "use_dynamic_team_credential": { + "name": "use_dynamic_team_credential", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "agent_tools_agent_id_agents_id_fk": { + "name": "agent_tools_agent_id_agents_id_fk", + "tableFrom": "agent_tools", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_tools_tool_id_tools_id_fk": { + "name": "agent_tools_tool_id_tools_id_fk", + "tableFrom": "agent_tools", + "tableTo": "tools", + "columnsFrom": [ + "tool_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_tools_credential_source_mcp_server_id_mcp_server_id_fk": { + "name": "agent_tools_credential_source_mcp_server_id_mcp_server_id_fk", + "tableFrom": "agent_tools", + "tableTo": "mcp_server", + "columnsFrom": [ + "credential_source_mcp_server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "agent_tools_execution_source_mcp_server_id_mcp_server_id_fk": { + "name": "agent_tools_execution_source_mcp_server_id_mcp_server_id_fk", + "tableFrom": "agent_tools", + "tableTo": "mcp_server", + "columnsFrom": [ + "execution_source_mcp_server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "agent_tools_agent_id_tool_id_unique": { + "name": "agent_tools_agent_id_tool_id_unique", + "nullsNotDistinct": false, + "columns": [ + "agent_id", + "tool_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "agent_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_demo": { + "name": "is_demo", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "consider_context_untrusted": { + "name": "consider_context_untrusted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "agent_type": { + "name": "agent_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'mcp_gateway'" + }, + "system_prompt": { + "name": "system_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_prompt": { + "name": "user_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_version": { + "name": "prompt_version", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "prompt_history": { + "name": "prompt_history", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "incoming_email_enabled": { + "name": "incoming_email_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "incoming_email_security_mode": { + "name": "incoming_email_security_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'private'" + }, + "incoming_email_allowed_domain": { + "name": "incoming_email_allowed_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "llm_api_key_id": { + "name": "llm_api_key_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "llm_model": { + "name": "llm_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "identity_provider_id": { + "name": "identity_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "built_in_agent_config": { + "name": "built_in_agent_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "built_in": { + "name": "built_in", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "\"agents\".\"built_in_agent_config\" IS NOT NULL", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agents_organization_id_idx": { + "name": "agents_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_agent_type_idx": { + "name": "agents_agent_type_idx", + "columns": [ + { + "expression": "agent_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_identity_provider_id_idx": { + "name": "agents_identity_provider_id_idx", + "columns": [ + { + "expression": "identity_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_author_id_idx": { + "name": "agents_author_id_idx", + "columns": [ + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_scope_idx": { + "name": "agents_scope_idx", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agents_author_id_user_id_fk": { + "name": "agents_author_id_user_id_fk", + "tableFrom": "agents", + "tableTo": "user", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "agents_llm_api_key_id_chat_api_keys_id_fk": { + "name": "agents_llm_api_key_id_chat_api_keys_id_fk", + "tableFrom": "agents", + "tableTo": "chat_api_keys", + "columnsFrom": [ + "llm_api_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "agents_identity_provider_id_identity_provider_id_fk": { + "name": "agents_identity_provider_id_identity_provider_id_fk", + "tableFrom": "agents", + "tableTo": "identity_provider", + "columnsFrom": [ + "identity_provider_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key_models": { + "name": "api_key_models", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "api_key_id": { + "name": "api_key_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "is_fastest": { + "name": "is_fastest", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_best": { + "name": "is_best", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "api_key_models_api_key_id_idx": { + "name": "api_key_models_api_key_id_idx", + "columns": [ + { + "expression": "api_key_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_models_model_id_idx": { + "name": "api_key_models_model_id_idx", + "columns": [ + { + "expression": "model_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_models_api_key_id_chat_api_keys_id_fk": { + "name": "api_key_models_api_key_id_chat_api_keys_id_fk", + "tableFrom": "api_key_models", + "tableTo": "chat_api_keys", + "columnsFrom": [ + "api_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_models_model_id_models_id_fk": { + "name": "api_key_models_model_id_models_id_fk", + "tableFrom": "api_key_models", + "tableTo": "models", + "columnsFrom": [ + "model_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_models_unique": { + "name": "api_key_models_unique", + "nullsNotDistinct": false, + "columns": [ + "api_key_id", + "model_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 86400000 + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 10 + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "apikey_user_id_user_id_fk": { + "name": "apikey_user_id_user_id_fk", + "tableFrom": "apikey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.browser_tab_states": { + "name": "browser_tab_states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isolation_key": { + "name": "isolation_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tab_index": { + "name": "tab_index", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "browser_tab_states_agent_user_isolation_idx": { + "name": "browser_tab_states_agent_user_isolation_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "isolation_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "browser_tab_states_user_id_idx": { + "name": "browser_tab_states_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "browser_tab_states_agent_id_agents_id_fk": { + "name": "browser_tab_states_agent_id_agents_id_fk", + "tableFrom": "browser_tab_states", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "browser_tab_states_user_id_user_id_fk": { + "name": "browser_tab_states_user_id_user_id_fk", + "tableFrom": "browser_tab_states", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_api_keys": { + "name": "chat_api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chat_api_keys_organization_id_idx": { + "name": "chat_api_keys_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_api_keys_org_provider_idx": { + "name": "chat_api_keys_org_provider_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_api_keys_system_unique": { + "name": "chat_api_keys_system_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat_api_keys\".\"is_system\" = true", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_api_keys_primary_personal_unique": { + "name": "chat_api_keys_primary_personal_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat_api_keys\".\"is_primary\" = true AND \"chat_api_keys\".\"scope\" = 'personal'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_api_keys_primary_team_unique": { + "name": "chat_api_keys_primary_team_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat_api_keys\".\"is_primary\" = true AND \"chat_api_keys\".\"scope\" = 'team'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_api_keys_primary_org_wide_unique": { + "name": "chat_api_keys_primary_org_wide_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat_api_keys\".\"is_primary\" = true AND \"chat_api_keys\".\"scope\" = 'org_wide'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_api_keys_secret_id_secret_id_fk": { + "name": "chat_api_keys_secret_id_secret_id_fk", + "tableFrom": "chat_api_keys", + "tableTo": "secret", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "chat_api_keys_user_id_user_id_fk": { + "name": "chat_api_keys_user_id_user_id_fk", + "tableFrom": "chat_api_keys", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_api_keys_team_id_team_id_fk": { + "name": "chat_api_keys_team_id_team_id_fk", + "tableFrom": "chat_api_keys", + "tableTo": "team", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chatops_channel_binding": { + "name": "chatops_channel_binding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "channel_name": { + "name": "channel_name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "workspace_name": { + "name": "workspace_name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "is_dm": { + "name": "is_dm", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dm_owner_email": { + "name": "dm_owner_email", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chatops_channel_binding_provider_channel_workspace_idx": { + "name": "chatops_channel_binding_provider_channel_workspace_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chatops_channel_binding_organization_id_idx": { + "name": "chatops_channel_binding_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chatops_channel_binding_agent_id_idx": { + "name": "chatops_channel_binding_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chatops_channel_binding_agent_id_agents_id_fk": { + "name": "chatops_channel_binding_agent_id_agents_id_fk", + "tableFrom": "chatops_channel_binding", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chatops_processed_message": { + "name": "chatops_processed_message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "message_id": { + "name": "message_id", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chatops_processed_message_processed_at_idx": { + "name": "chatops_processed_message_processed_at_idx", + "columns": [ + { + "expression": "processed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "chatops_processed_message_message_id_unique": { + "name": "chatops_processed_message_message_id_unique", + "nullsNotDistinct": false, + "columns": [ + "message_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connector_runs": { + "name": "connector_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "connector_id": { + "name": "connector_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "documents_processed": { + "name": "documents_processed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "documents_ingested": { + "name": "documents_ingested", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "total_items": { + "name": "total_items", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_batches": { + "name": "total_batches", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "completed_batches": { + "name": "completed_batches", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "item_errors": { + "name": "item_errors", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logs": { + "name": "logs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checkpoint": { + "name": "checkpoint", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "connector_runs_connector_id_idx": { + "name": "connector_runs_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "connector_runs_connector_id_knowledge_base_connectors_id_fk": { + "name": "connector_runs_connector_id_knowledge_base_connectors_id_fk", + "tableFrom": "connector_runs", + "tableTo": "knowledge_base_connectors", + "columnsFrom": [ + "connector_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conversation_enabled_tools": { + "name": "conversation_enabled_tools", + "schema": "", + "columns": { + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tool_id": { + "name": "tool_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "conversation_enabled_tools_conversation_id_conversations_id_fk": { + "name": "conversation_enabled_tools_conversation_id_conversations_id_fk", + "tableFrom": "conversation_enabled_tools", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversation_enabled_tools_tool_id_tools_id_fk": { + "name": "conversation_enabled_tools_tool_id_tools_id_fk", + "tableFrom": "conversation_enabled_tools", + "tableTo": "tools", + "columnsFrom": [ + "tool_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "conversation_enabled_tools_conversation_id_tool_id_pk": { + "name": "conversation_enabled_tools_conversation_id_tool_id_pk", + "columns": [ + "conversation_id", + "tool_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conversation_shares": { + "name": "conversation_shares", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "conversation_share_visibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "conversation_shares_conversation_id_conversations_id_fk": { + "name": "conversation_shares_conversation_id_conversations_id_fk", + "tableFrom": "conversation_shares", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "conversation_shares_conversation_id_unique": { + "name": "conversation_shares_conversation_id_unique", + "nullsNotDistinct": false, + "columns": [ + "conversation_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conversations": { + "name": "conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_api_key_id": { + "name": "chat_api_key_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "selected_model": { + "name": "selected_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'gpt-4o'" + }, + "selected_provider": { + "name": "selected_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_custom_tool_selection": { + "name": "has_custom_tool_selection", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "todo_list": { + "name": "todo_list", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "artifact": { + "name": "artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "conversations_agent_id_agents_id_fk": { + "name": "conversations_agent_id_agents_id_fk", + "tableFrom": "conversations", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "conversations_chat_api_key_id_chat_api_keys_id_fk": { + "name": "conversations_chat_api_key_id_chat_api_keys_id_fk", + "tableFrom": "conversations", + "tableTo": "chat_api_keys", + "columnsFrom": [ + "chat_api_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dual_llm_config": { + "name": "dual_llm_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "main_agent_prompt": { + "name": "main_agent_prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quarantined_agent_prompt": { + "name": "quarantined_agent_prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary_prompt": { + "name": "summary_prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "max_rounds": { + "name": "max_rounds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dual_llm_results": { + "name": "dual_llm_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversations": { + "name": "conversations", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "dual_llm_results_agent_id_idx": { + "name": "dual_llm_results_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dual_llm_results_agent_id_agents_id_fk": { + "name": "dual_llm_results_agent_id_agents_id_fk", + "tableFrom": "dual_llm_results", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.identity_provider": { + "name": "identity_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role_mapping": { + "name": "role_mapping", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team_sync_config": { + "name": "team_sync_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain_verified": { + "name": "domain_verified", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "identity_provider_user_id_user_id_fk": { + "name": "identity_provider_user_id_user_id_fk", + "tableFrom": "identity_provider", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "identity_provider_provider_id_unique": { + "name": "identity_provider_provider_id_unique", + "nullsNotDistinct": false, + "columns": [ + "provider_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.incoming_email_subscription": { + "name": "incoming_email_subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "subscription_id": { + "name": "subscription_id", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "webhook_url": { + "name": "webhook_url", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": true + }, + "client_state": { + "name": "client_state", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interactions": { + "name": "interactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "external_agent_id": { + "name": "external_agent_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "session_source": { + "name": "session_source", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "request": { + "name": "request", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "processed_request": { + "name": "processed_request", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response": { + "name": "response", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "baseline_model": { + "name": "baseline_model", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "baseline_cost": { + "name": "baseline_cost", + "type": "numeric(13, 10)", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric(13, 10)", + "primaryKey": false, + "notNull": false + }, + "toon_tokens_before": { + "name": "toon_tokens_before", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "toon_tokens_after": { + "name": "toon_tokens_after", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "toon_cost_savings": { + "name": "toon_cost_savings", + "type": "numeric(13, 10)", + "primaryKey": false, + "notNull": false + }, + "toon_skip_reason": { + "name": "toon_skip_reason", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "interactions_agent_id_idx": { + "name": "interactions_agent_id_idx", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "interactions_external_agent_id_idx": { + "name": "interactions_external_agent_id_idx", + "columns": [ + { + "expression": "external_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "interactions_execution_id_idx": { + "name": "interactions_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "interactions_user_id_idx": { + "name": "interactions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "interactions_session_id_idx": { + "name": "interactions_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "interactions_created_at_idx": { + "name": "interactions_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "interactions_profile_created_at_idx": { + "name": "interactions_profile_created_at_idx", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "interactions_session_created_at_idx": { + "name": "interactions_session_created_at_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "interactions_profile_id_agents_id_fk": { + "name": "interactions_profile_id_agents_id_fk", + "tableFrom": "interactions", + "tableTo": "agents", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "interactions_user_id_user_id_fk": { + "name": "interactions_user_id_user_id_fk", + "tableFrom": "interactions", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.internal_mcp_catalog": { + "name": "internal_mcp_catalog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "instructions": { + "name": "instructions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installation_command": { + "name": "installation_command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requires_auth": { + "name": "requires_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "auth_description": { + "name": "auth_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_fields": { + "name": "auth_fields", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "server_type": { + "name": "server_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "docs_url": { + "name": "docs_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret_id": { + "name": "client_secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "local_config_secret_id": { + "name": "local_config_secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "local_config": { + "name": "local_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "deployment_spec_yaml": { + "name": "deployment_spec_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_config": { + "name": "user_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "oauth_config": { + "name": "oauth_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "mcp_catalog_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'org'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "internal_mcp_catalog_organization_id_idx": { + "name": "internal_mcp_catalog_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "internal_mcp_catalog_author_id_idx": { + "name": "internal_mcp_catalog_author_id_idx", + "columns": [ + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "internal_mcp_catalog_scope_idx": { + "name": "internal_mcp_catalog_scope_idx", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "internal_mcp_catalog_client_secret_id_secret_id_fk": { + "name": "internal_mcp_catalog_client_secret_id_secret_id_fk", + "tableFrom": "internal_mcp_catalog", + "tableTo": "secret", + "columnsFrom": [ + "client_secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "internal_mcp_catalog_local_config_secret_id_secret_id_fk": { + "name": "internal_mcp_catalog_local_config_secret_id_secret_id_fk", + "tableFrom": "internal_mcp_catalog", + "tableTo": "secret", + "columnsFrom": [ + "local_config_secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "internal_mcp_catalog_author_id_user_id_fk": { + "name": "internal_mcp_catalog_author_id_user_id_fk", + "tableFrom": "internal_mcp_catalog", + "tableTo": "user", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.jwks": { + "name": "jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kb_chunks": { + "name": "kb_chunks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "search_vector": { + "name": "search_vector", + "type": "tsvector", + "primaryKey": false, + "notNull": false + }, + "acl": { + "name": "acl", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_chunks_document_id_idx": { + "name": "kb_chunks_document_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kb_chunks_document_id_kb_documents_id_fk": { + "name": "kb_chunks_document_id_kb_documents_id_fk", + "tableFrom": "kb_chunks", + "tableTo": "kb_documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kb_documents": { + "name": "kb_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acl": { + "name": "acl", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "embedding_status": { + "name": "embedding_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_documents_org_id_idx": { + "name": "kb_documents_org_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_documents_source_idx": { + "name": "kb_documents_source_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kb_documents_connector_id_knowledge_base_connectors_id_fk": { + "name": "kb_documents_connector_id_knowledge_base_connectors_id_fk", + "tableFrom": "kb_documents", + "tableTo": "knowledge_base_connectors", + "columnsFrom": [ + "connector_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_connector_assignment": { + "name": "knowledge_base_connector_assignment", + "schema": "", + "columns": { + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_connector_assignment_kb_id_idx": { + "name": "kb_connector_assignment_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_connector_assignment_connector_id_idx": { + "name": "kb_connector_assignment_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_connector_assignment_knowledge_base_id_knowledge_bases_id_fk": { + "name": "knowledge_base_connector_assignment_knowledge_base_id_knowledge_bases_id_fk", + "tableFrom": "knowledge_base_connector_assignment", + "tableTo": "knowledge_bases", + "columnsFrom": [ + "knowledge_base_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_connector_assignment_connector_id_knowledge_base_connectors_id_fk": { + "name": "knowledge_base_connector_assignment_connector_id_knowledge_base_connectors_id_fk", + "tableFrom": "knowledge_base_connector_assignment", + "tableTo": "knowledge_base_connectors", + "columnsFrom": [ + "connector_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_connectors": { + "name": "knowledge_base_connectors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'0 */6 * * *'" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sync_status": { + "name": "last_sync_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checkpoint": { + "name": "checkpoint", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "knowledge_base_connectors_organization_id_idx": { + "name": "knowledge_base_connectors_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_connectors_secret_id_secret_id_fk": { + "name": "knowledge_base_connectors_secret_id_secret_id_fk", + "tableFrom": "knowledge_base_connectors", + "tableTo": "secret", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_bases": { + "name": "knowledge_bases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'org-wide'" + }, + "team_ids": { + "name": "team_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "knowledge_bases_organization_id_idx": { + "name": "knowledge_bases_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.label_keys": { + "name": "label_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "label_keys_key_unique": { + "name": "label_keys_key_unique", + "nullsNotDistinct": false, + "columns": [ + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.label_values": { + "name": "label_values", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "label_values_value_unique": { + "name": "label_values_value_unique", + "nullsNotDistinct": false, + "columns": [ + "value" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.limit_model_usage": { + "name": "limit_model_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "limit_id": { + "name": "limit_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "current_usage_tokens_in": { + "name": "current_usage_tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "current_usage_tokens_out": { + "name": "current_usage_tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "limit_model_usage_limit_id_idx": { + "name": "limit_model_usage_limit_id_idx", + "columns": [ + { + "expression": "limit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "limit_model_usage_limit_model_idx": { + "name": "limit_model_usage_limit_model_idx", + "columns": [ + { + "expression": "limit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "limit_model_usage_limit_id_limits_id_fk": { + "name": "limit_model_usage_limit_id_limits_id_fk", + "tableFrom": "limit_model_usage", + "tableTo": "limits", + "columnsFrom": [ + "limit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.limits": { + "name": "limits", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "entity_type": { + "name": "entity_type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "limit_type": { + "name": "limit_type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "limit_value": { + "name": "limit_value", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mcp_server_name": { + "name": "mcp_server_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "tool_name": { + "name": "tool_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_cleanup": { + "name": "last_cleanup", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "limits_entity_idx": { + "name": "limits_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "limits_type_idx": { + "name": "limits_type_idx", + "columns": [ + { + "expression": "limit_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_catalog_labels": { + "name": "mcp_catalog_labels", + "schema": "", + "columns": { + "catalog_id": { + "name": "catalog_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "value_id": { + "name": "value_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mcp_catalog_labels_catalog_id_internal_mcp_catalog_id_fk": { + "name": "mcp_catalog_labels_catalog_id_internal_mcp_catalog_id_fk", + "tableFrom": "mcp_catalog_labels", + "tableTo": "internal_mcp_catalog", + "columnsFrom": [ + "catalog_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_catalog_labels_key_id_label_keys_id_fk": { + "name": "mcp_catalog_labels_key_id_label_keys_id_fk", + "tableFrom": "mcp_catalog_labels", + "tableTo": "label_keys", + "columnsFrom": [ + "key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_catalog_labels_value_id_label_values_id_fk": { + "name": "mcp_catalog_labels_value_id_label_values_id_fk", + "tableFrom": "mcp_catalog_labels", + "tableTo": "label_values", + "columnsFrom": [ + "value_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "mcp_catalog_labels_catalog_id_key_id_pk": { + "name": "mcp_catalog_labels_catalog_id_key_id_pk", + "columns": [ + "catalog_id", + "key_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_catalog_team": { + "name": "mcp_catalog_team", + "schema": "", + "columns": { + "catalog_id": { + "name": "catalog_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mcp_catalog_team_catalog_id_internal_mcp_catalog_id_fk": { + "name": "mcp_catalog_team_catalog_id_internal_mcp_catalog_id_fk", + "tableFrom": "mcp_catalog_team", + "tableTo": "internal_mcp_catalog", + "columnsFrom": [ + "catalog_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_catalog_team_team_id_team_id_fk": { + "name": "mcp_catalog_team_team_id_team_id_fk", + "tableFrom": "mcp_catalog_team", + "tableTo": "team", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "mcp_catalog_team_catalog_id_team_id_pk": { + "name": "mcp_catalog_team_catalog_id_team_id_pk", + "columns": [ + "catalog_id", + "team_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_http_sessions": { + "name": "mcp_http_sessions", + "schema": "", + "columns": { + "connection_key": { + "name": "connection_key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_endpoint_url": { + "name": "session_endpoint_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_endpoint_pod_name": { + "name": "session_endpoint_pod_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_server_installation_request": { + "name": "mcp_server_installation_request", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "external_catalog_id": { + "name": "external_catalog_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_by": { + "name": "requested_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "request_reason": { + "name": "request_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_server_config": { + "name": "custom_server_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'null'::jsonb" + }, + "admin_response": { + "name": "admin_response", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reviewed_by": { + "name": "reviewed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mcp_server_installation_request_requested_by_user_id_fk": { + "name": "mcp_server_installation_request_requested_by_user_id_fk", + "tableFrom": "mcp_server_installation_request", + "tableTo": "user", + "columnsFrom": [ + "requested_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_installation_request_reviewed_by_user_id_fk": { + "name": "mcp_server_installation_request_reviewed_by_user_id_fk", + "tableFrom": "mcp_server_installation_request", + "tableTo": "user", + "columnsFrom": [ + "reviewed_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_server_user": { + "name": "mcp_server_user", + "schema": "", + "columns": { + "mcp_server_id": { + "name": "mcp_server_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mcp_server_user_mcp_server_id_mcp_server_id_fk": { + "name": "mcp_server_user_mcp_server_id_mcp_server_id_fk", + "tableFrom": "mcp_server_user", + "tableTo": "mcp_server", + "columnsFrom": [ + "mcp_server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_user_user_id_user_id_fk": { + "name": "mcp_server_user_user_id_user_id_fk", + "tableFrom": "mcp_server_user", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "mcp_server_user_mcp_server_id_user_id_pk": { + "name": "mcp_server_user_mcp_server_id_user_id_pk", + "columns": [ + "mcp_server_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_server": { + "name": "mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog_id": { + "name": "catalog_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "server_type": { + "name": "server_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reinstall_required": { + "name": "reinstall_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "local_installation_status": { + "name": "local_installation_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "local_installation_error": { + "name": "local_installation_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_refresh_error": { + "name": "oauth_refresh_error", + "type": "oauth_refresh_error_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "oauth_refresh_failed_at": { + "name": "oauth_refresh_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mcp_server_catalog_id_internal_mcp_catalog_id_fk": { + "name": "mcp_server_catalog_id_internal_mcp_catalog_id_fk", + "tableFrom": "mcp_server", + "tableTo": "internal_mcp_catalog", + "columnsFrom": [ + "catalog_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_server_secret_id_secret_id_fk": { + "name": "mcp_server_secret_id_secret_id_fk", + "tableFrom": "mcp_server", + "tableTo": "secret", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_server_owner_id_user_id_fk": { + "name": "mcp_server_owner_id_user_id_fk", + "tableFrom": "mcp_server", + "tableTo": "user", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_server_team_id_team_id_fk": { + "name": "mcp_server_team_id_team_id_fk", + "tableFrom": "mcp_server", + "tableTo": "team", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_tool_calls": { + "name": "mcp_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "mcp_server_name": { + "name": "mcp_server_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "method": { + "name": "method", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tool_call": { + "name": "tool_call", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "tool_result": { + "name": "tool_result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_method": { + "name": "auth_method", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_tool_calls_agent_id_idx": { + "name": "mcp_tool_calls_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_tool_calls_created_at_idx": { + "name": "mcp_tool_calls_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_tool_calls_agent_id_agents_id_fk": { + "name": "mcp_tool_calls_agent_id_agents_id_fk", + "tableFrom": "mcp_tool_calls", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_tool_calls_user_id_user_id_fk": { + "name": "mcp_tool_calls_user_id_user_id_fk", + "tableFrom": "mcp_tool_calls", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "default_agent_id": { + "name": "default_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_default_agent_id_agents_id_fk": { + "name": "member_default_agent_id_agents_id_fk", + "tableFrom": "member", + "tableTo": "agents", + "columnsFrom": [ + "default_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.messages": { + "name": "messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "messages_conversation_id_idx": { + "name": "messages_conversation_id_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "messages_conversation_id_conversations_id_fk": { + "name": "messages_conversation_id_conversations_id_fk", + "tableFrom": "messages", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.models": { + "name": "models", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context_length": { + "name": "context_length", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "input_modalities": { + "name": "input_modalities", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "output_modalities": { + "name": "output_modalities", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "supports_tool_calling": { + "name": "supports_tool_calling", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "prompt_price_per_token": { + "name": "prompt_price_per_token", + "type": "numeric(20, 12)", + "primaryKey": false, + "notNull": false + }, + "completion_price_per_token": { + "name": "completion_price_per_token", + "type": "numeric(20, 12)", + "primaryKey": false, + "notNull": false + }, + "custom_price_per_million_input": { + "name": "custom_price_per_million_input", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "custom_price_per_million_output": { + "name": "custom_price_per_million_output", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "discovered_via_llm_proxy": { + "name": "discovered_via_llm_proxy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "models_provider_model_idx": { + "name": "models_provider_model_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "models_external_id_idx": { + "name": "models_external_id_idx", + "columns": [ + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "models_provider_model_unique": { + "name": "models_provider_model_unique", + "nullsNotDistinct": false, + "columns": [ + "provider", + "model_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_access_token": { + "name": "oauth_access_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_id": { + "name": "refresh_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "oauth_access_token_client_id_oauth_client_client_id_fk": { + "name": "oauth_access_token_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_session_id_session_id_fk": { + "name": "oauth_access_token_session_id_session_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_access_token_user_id_user_id_fk": { + "name": "oauth_access_token_user_id_user_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_refresh_id_oauth_refresh_token_id_fk": { + "name": "oauth_access_token_refresh_id_oauth_refresh_token_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_refresh_token", + "columnsFrom": [ + "refresh_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_access_token_token_unique": { + "name": "oauth_access_token_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_client": { + "name": "oauth_client", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contacts": { + "name": "contacts", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "tos": { + "name": "tos", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "policy": { + "name": "policy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_id": { + "name": "software_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_version": { + "name": "software_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_statement": { + "name": "software_statement", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "skip_consent": { + "name": "skip_consent", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enable_end_session": { + "name": "enable_end_session", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_types": { + "name": "grant_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "response_types": { + "name": "response_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "oauth_client_user_id_user_id_fk": { + "name": "oauth_client_user_id_user_id_fk", + "tableFrom": "oauth_client", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_client_client_id_unique": { + "name": "oauth_client_client_id_unique", + "nullsNotDistinct": false, + "columns": [ + "client_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_consent": { + "name": "oauth_consent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "oauth_consent_client_id_oauth_client_client_id_fk": { + "name": "oauth_consent_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "oauth_client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_consent_user_id_user_id_fk": { + "name": "oauth_consent_user_id_user_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_refresh_token": { + "name": "oauth_refresh_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked": { + "name": "revoked", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "oauth_refresh_token_client_id_oauth_client_client_id_fk": { + "name": "oauth_refresh_token_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "oauth_client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_refresh_token_session_id_session_id_fk": { + "name": "oauth_refresh_token_session_id_session_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_refresh_token_user_id_user_id_fk": { + "name": "oauth_refresh_token_user_id_user_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.optimization_rules": { + "name": "optimization_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "entity_type": { + "name": "entity_type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conditions": { + "name": "conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_model": { + "name": "target_model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "optimization_rules_entity_idx": { + "name": "optimization_rules_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_role": { + "name": "organization_role", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "organization_role_organization_id_organization_id_fk": { + "name": "organization_role_organization_id_organization_id_fk", + "tableFrom": "organization_role", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_role_organization_id_role_unique": { + "name": "organization_role_organization_id_role_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "role" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo_dark": { + "name": "logo_dark", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "limit_cleanup_interval": { + "name": "limit_cleanup_interval", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'1h'" + }, + "onboarding_complete": { + "name": "onboarding_complete", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'cosmic-night'" + }, + "custom_font": { + "name": "custom_font", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'lato'" + }, + "convert_tool_results_to_toon": { + "name": "convert_tool_results_to_toon", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "compression_scope": { + "name": "compression_scope", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "global_tool_policy": { + "name": "global_tool_policy", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'permissive'" + }, + "allow_chat_file_uploads": { + "name": "allow_chat_file_uploads", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "embedding_chat_api_key_id": { + "name": "embedding_chat_api_key_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reranker_chat_api_key_id": { + "name": "reranker_chat_api_key_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reranker_model": { + "name": "reranker_model", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.processed_email": { + "name": "processed_email", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "message_id": { + "name": "message_id", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "processed_email_processed_at_idx": { + "name": "processed_email_processed_at_idx", + "columns": [ + { + "expression": "processed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "processed_email_message_id_unique": { + "name": "processed_email_message_id_unique", + "nullsNotDistinct": false, + "columns": [ + "message_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.secret": { + "name": "secret", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true, + "default": "'secret'" + }, + "secret": { + "name": "secret", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "is_vault": { + "name": "is_vault", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_byos_vault": { + "name": "is_byos_vault", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tasks": { + "name": "tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "task_type": { + "name": "task_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "scheduled_for": { + "name": "scheduled_for", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "periodic": { + "name": "periodic", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tasks_dequeue_idx": { + "name": "tasks_dequeue_idx", + "columns": [ + { + "expression": "task_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scheduled_for", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_unique_periodic_idx": { + "name": "tasks_unique_periodic_idx", + "columns": [ + { + "expression": "task_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"tasks\".\"periodic\" = true AND \"tasks\".\"status\" IN ('pending', 'processing')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_external_group": { + "name": "team_external_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_identifier": { + "name": "group_identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_external_group_team_id_team_id_fk": { + "name": "team_external_group_team_id_team_id_fk", + "tableFrom": "team_external_group", + "tableTo": "team", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "team_external_group_team_group_unique": { + "name": "team_external_group_team_group_unique", + "nullsNotDistinct": false, + "columns": [ + "team_id", + "group_identifier" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_member": { + "name": "team_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "synced_from_sso": { + "name": "synced_from_sso", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "team_member_team_id_team_id_fk": { + "name": "team_member_team_id_team_id_fk", + "tableFrom": "team_member", + "tableTo": "team", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_member_user_id_user_id_fk": { + "name": "team_member_user_id_user_id_fk", + "tableFrom": "team_member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_token": { + "name": "team_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_organization_token": { + "name": "is_organization_token", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token_start": { + "name": "token_start", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_team_token_org_id": { + "name": "idx_team_token_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_team_token_team_id": { + "name": "idx_team_token_team_id", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_token_organization_id_organization_id_fk": { + "name": "team_token_organization_id_organization_id_fk", + "tableFrom": "team_token", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_token_team_id_team_id_fk": { + "name": "team_token_team_id_team_id_fk", + "tableFrom": "team_token", + "tableTo": "team", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_token_secret_id_secret_id_fk": { + "name": "team_token_secret_id_secret_id_fk", + "tableFrom": "team_token", + "tableTo": "secret", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "team_token_organization_id_team_id_unique": { + "name": "team_token_organization_id_team_id_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "team_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_vault_folder": { + "name": "team_vault_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vault_path": { + "name": "vault_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "team_vault_folder_team_id_team_id_fk": { + "name": "team_vault_folder_team_id_team_id_fk", + "tableFrom": "team_vault_folder", + "tableTo": "team", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "team_vault_folder_team_id_unique": { + "name": "team_vault_folder_team_id_unique", + "nullsNotDistinct": false, + "columns": [ + "team_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team": { + "name": "team", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "convert_tool_results_to_toon": { + "name": "convert_tool_results_to_toon", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "team_organization_id_organization_id_fk": { + "name": "team_organization_id_organization_id_fk", + "tableFrom": "team", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_created_by_user_id_fk": { + "name": "team_created_by_user_id_fk", + "tableFrom": "team", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_invocation_policies": { + "name": "tool_invocation_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tool_id": { + "name": "tool_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "conditions": { + "name": "conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "tool_invocation_policies_tool_id_tools_id_fk": { + "name": "tool_invocation_policies_tool_id_tools_id_fk", + "tableFrom": "tool_invocation_policies", + "tableTo": "tools", + "columnsFrom": [ + "tool_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tools": { + "name": "tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "catalog_id": { + "name": "catalog_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "delegate_to_agent_id": { + "name": "delegate_to_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parameters": { + "name": "parameters", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "policies_auto_configured_at": { + "name": "policies_auto_configured_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "policies_auto_configuring_started_at": { + "name": "policies_auto_configuring_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "policies_auto_configured_reasoning": { + "name": "policies_auto_configured_reasoning", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "policies_auto_configured_model": { + "name": "policies_auto_configured_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tools_delegate_to_agent_id_idx": { + "name": "tools_delegate_to_agent_id_idx", + "columns": [ + { + "expression": "delegate_to_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tools_agent_id_agents_id_fk": { + "name": "tools_agent_id_agents_id_fk", + "tableFrom": "tools", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tools_catalog_id_internal_mcp_catalog_id_fk": { + "name": "tools_catalog_id_internal_mcp_catalog_id_fk", + "tableFrom": "tools", + "tableTo": "internal_mcp_catalog", + "columnsFrom": [ + "catalog_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tools_mcp_server_id_mcp_server_id_fk": { + "name": "tools_mcp_server_id_mcp_server_id_fk", + "tableFrom": "tools", + "tableTo": "mcp_server", + "columnsFrom": [ + "mcp_server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tools_delegate_to_agent_id_agents_id_fk": { + "name": "tools_delegate_to_agent_id_agents_id_fk", + "tableFrom": "tools", + "tableTo": "agents", + "columnsFrom": [ + "delegate_to_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tools_catalog_id_name_agent_id_delegate_to_agent_id_unique": { + "name": "tools_catalog_id_name_agent_id_delegate_to_agent_id_unique", + "nullsNotDistinct": false, + "columns": [ + "catalog_id", + "name", + "agent_id", + "delegate_to_agent_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trusted_data_policies": { + "name": "trusted_data_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tool_id": { + "name": "tool_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "conditions": { + "name": "conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'mark_as_trusted'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "trusted_data_policies_tool_id_tools_id_fk": { + "name": "trusted_data_policies_tool_id_tools_id_fk", + "tableFrom": "trusted_data_policies", + "tableTo": "tools", + "columnsFrom": [ + "tool_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "two_factor_user_id_user_id_fk": { + "name": "two_factor_user_id_user_id_fk", + "tableFrom": "two_factor", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_token": { + "name": "user_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token_start": { + "name": "token_start", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_user_token_org_id": { + "name": "idx_user_token_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_token_user_id": { + "name": "idx_user_token_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_token_organization_id_organization_id_fk": { + "name": "user_token_organization_id_organization_id_fk", + "tableFrom": "user_token", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_token_user_id_user_id_fk": { + "name": "user_token_user_id_user_id_fk", + "tableFrom": "user_token", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_token_secret_id_secret_id_fk": { + "name": "user_token_secret_id_secret_id_fk", + "tableFrom": "user_token", + "tableTo": "secret", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_token_organization_id_user_id_unique": { + "name": "user_token_organization_id_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.virtual_api_keys": { + "name": "virtual_api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_api_key_id": { + "name": "chat_api_key_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token_start": { + "name": "token_start", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_virtual_api_key_chat_api_key_id": { + "name": "idx_virtual_api_key_chat_api_key_id", + "columns": [ + { + "expression": "chat_api_key_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_virtual_api_key_token_start": { + "name": "idx_virtual_api_key_token_start", + "columns": [ + { + "expression": "token_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "virtual_api_keys_chat_api_key_id_chat_api_keys_id_fk": { + "name": "virtual_api_keys_chat_api_key_id_chat_api_keys_id_fk", + "tableFrom": "virtual_api_keys", + "tableTo": "chat_api_keys", + "columnsFrom": [ + "chat_api_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "virtual_api_keys_secret_id_secret_id_fk": { + "name": "virtual_api_keys_secret_id_secret_id_fk", + "tableFrom": "virtual_api_keys", + "tableTo": "secret", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.agent_scope": { + "name": "agent_scope", + "schema": "public", + "values": [ + "personal", + "team", + "org" + ] + }, + "public.conversation_share_visibility": { + "name": "conversation_share_visibility", + "schema": "public", + "values": [ + "organization" + ] + }, + "public.mcp_catalog_scope": { + "name": "mcp_catalog_scope", + "schema": "public", + "values": [ + "personal", + "team", + "org" + ] + }, + "public.oauth_refresh_error_enum": { + "name": "oauth_refresh_error_enum", + "schema": "public", + "values": [ + "refresh_failed", + "no_refresh_token" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/platform/backend/src/database/migrations/meta/_journal.json b/platform/backend/src/database/migrations/meta/_journal.json index 59113fd13e..bc4b845f67 100644 --- a/platform/backend/src/database/migrations/meta/_journal.json +++ b/platform/backend/src/database/migrations/meta/_journal.json @@ -1226,6 +1226,13 @@ "when": 1773082514382, "tag": "0174_unusual_ricochet", "breakpoints": true + }, + { + "idx": 175, + "version": "7", + "when": 1773157827567, + "tag": "0175_colossal_aaron_stack", + "breakpoints": true } ] } \ No newline at end of file diff --git a/platform/backend/src/database/schemas/organization-role.ts b/platform/backend/src/database/schemas/organization-role.ts index 880c2ec1b9..1e96c7dfea 100644 --- a/platform/backend/src/database/schemas/organization-role.ts +++ b/platform/backend/src/database/schemas/organization-role.ts @@ -10,6 +10,7 @@ export const organizationRole = pgTable( .references(() => organizationsTable.id, { onDelete: "cascade" }), role: text("role").notNull(), // Immutable identifier (lowercase, no spaces) - used by better-auth name: text("name").notNull(), // Editable display name - shown in UI + description: text("description"), permission: text("permission").notNull(), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").$onUpdate( diff --git a/platform/backend/src/knowledge-base/connectors/confluence/confluence-connector.test.ts b/platform/backend/src/knowledge-base/connectors/confluence/confluence-connector.test.ts index b04e400915..4995029eb0 100644 --- a/platform/backend/src/knowledge-base/connectors/confluence/confluence-connector.test.ts +++ b/platform/backend/src/knowledge-base/connectors/confluence/confluence-connector.test.ts @@ -10,11 +10,16 @@ import { // Mock confluence.js SDK const mockGetSpaces = vi.fn(); const mockSearchContentByCQL = vi.fn(); +const capturedConfluenceConfigs: Record[] = []; vi.mock("confluence.js", () => ({ ConfluenceClient: class MockConfluenceClient { space = { getSpaces: mockGetSpaces }; content = { searchContentByCQL: mockSearchContentByCQL }; + // biome-ignore lint/suspicious/noExplicitAny: mock constructor + constructor(config: any) { + capturedConfluenceConfigs.push(config); + } }, })); @@ -34,6 +39,7 @@ describe("ConfluenceConnector", () => { beforeEach(() => { vi.clearAllMocks(); + capturedConfluenceConfigs.length = 0; connector = new ConfluenceConnector(); }); @@ -127,6 +133,46 @@ describe("ConfluenceConnector", () => { expect(result.success).toBe(false); expect(result.error).toContain("Invalid Confluence configuration"); }); + + test("uses basic auth for server when email is provided", async () => { + mockGetSpaces.mockResolvedValueOnce({ results: [] }); + + await connector.testConnection({ + config: { ...validConfig, isCloud: false }, + credentials: { email: "admin", apiToken: "password123" }, + }); + + const config = capturedConfluenceConfigs[0]; + expect(config?.authentication).toEqual({ + basic: { email: "admin", apiToken: "password123" }, + }); + }); + + test("uses oauth2 (PAT) auth for server when email is not provided", async () => { + mockGetSpaces.mockResolvedValueOnce({ results: [] }); + + await connector.testConnection({ + config: { ...validConfig, isCloud: false }, + credentials: { apiToken: "pat-token-value" }, + }); + + const config = capturedConfluenceConfigs[0]; + expect(config?.authentication).toEqual({ + oauth2: { accessToken: "pat-token-value" }, + }); + }); + + test("sets noCheckAtlassianToken", async () => { + mockGetSpaces.mockResolvedValueOnce({ results: [] }); + + await connector.testConnection({ + config: { ...validConfig, isCloud: false }, + credentials: { apiToken: "pat-token" }, + }); + + const config = capturedConfluenceConfigs[0]; + expect(config?.noCheckAtlassianToken).toBe(true); + }); }); describe("sync", () => { diff --git a/platform/backend/src/knowledge-base/connectors/confluence/confluence-connector.ts b/platform/backend/src/knowledge-base/connectors/confluence/confluence-connector.ts index 1bc8d88fd5..7d9b515f5e 100644 --- a/platform/backend/src/knowledge-base/connectors/confluence/confluence-connector.ts +++ b/platform/backend/src/knowledge-base/connectors/confluence/confluence-connector.ts @@ -169,12 +169,10 @@ function createConfluenceClient( const host = config.confluenceUrl.replace(/\/+$/, ""); return new ConfluenceClient({ host, - authentication: { - basic: { - email: credentials.email ?? "", - apiToken: credentials.apiToken, - }, - }, + noCheckAtlassianToken: true, + authentication: credentials.email + ? { basic: { email: credentials.email, apiToken: credentials.apiToken } } + : { oauth2: { accessToken: credentials.apiToken } }, apiPrefix: config.isCloud ? "/wiki/rest/" : "/rest/", }); } diff --git a/platform/backend/src/knowledge-base/connectors/jira/jira-connector.test.ts b/platform/backend/src/knowledge-base/connectors/jira/jira-connector.test.ts index 5e6454d674..09177e5302 100644 --- a/platform/backend/src/knowledge-base/connectors/jira/jira-connector.test.ts +++ b/platform/backend/src/knowledge-base/connectors/jira/jira-connector.test.ts @@ -10,16 +10,22 @@ import { // Mock jira.js SDK const mockGetCurrentUser = vi.fn(); const mockEnhancedSearchPost = vi.fn(); +const mockSearchForIssuesUsingJql = vi.fn(); +const capturedConfigs: { type: string; config: Record }[] = []; vi.mock("jira.js", () => ({ ClientType: { Version2: "Version2", Version3: "Version3" }, // biome-ignore lint/suspicious/noExplicitAny: mock factory - createClient: (_type: any, _config: any) => ({ - myself: { getCurrentUser: mockGetCurrentUser }, - issueSearch: { - searchForIssuesUsingJqlEnhancedSearchPost: mockEnhancedSearchPost, - }, - }), + createClient: (type: any, config: any) => { + capturedConfigs.push({ type, config }); + return { + myself: { getCurrentUser: mockGetCurrentUser }, + issueSearch: { + searchForIssuesUsingJqlEnhancedSearchPost: mockEnhancedSearchPost, + searchForIssuesUsingJql: mockSearchForIssuesUsingJql, + }, + }; + }, })); describe("JiraConnector", () => { @@ -38,6 +44,7 @@ describe("JiraConnector", () => { beforeEach(() => { vi.clearAllMocks(); + capturedConfigs.length = 0; connector = new JiraConnector(); }); @@ -134,6 +141,68 @@ describe("JiraConnector", () => { expect(result.success).toBe(false); expect(result.error).toContain("Invalid Jira configuration"); }); + + test("uses basic auth for server when email is provided", async () => { + mockGetCurrentUser.mockResolvedValueOnce({ displayName: "User" }); + + await connector.testConnection({ + config: { ...validConfig, isCloud: false }, + credentials: { email: "admin", apiToken: "password123" }, + }); + + const serverConfig = capturedConfigs.find( + (c) => c.type === "Version2", + )?.config; + expect(serverConfig?.authentication).toEqual({ + basic: { email: "admin", apiToken: "password123" }, + }); + }); + + test("uses oauth2 (PAT) auth for server when email is not provided", async () => { + mockGetCurrentUser.mockResolvedValueOnce({ displayName: "User" }); + + await connector.testConnection({ + config: { ...validConfig, isCloud: false }, + credentials: { apiToken: "pat-token-value" }, + }); + + const serverConfig = capturedConfigs.find( + (c) => c.type === "Version2", + )?.config; + expect(serverConfig?.authentication).toEqual({ + oauth2: { accessToken: "pat-token-value" }, + }); + }); + + test("sets noCheckAtlassianToken for server instances", async () => { + mockGetCurrentUser.mockResolvedValueOnce({ displayName: "User" }); + + await connector.testConnection({ + config: { ...validConfig, isCloud: false }, + credentials: { apiToken: "pat-token" }, + }); + + const serverConfig = capturedConfigs.find( + (c) => c.type === "Version2", + )?.config; + expect(serverConfig?.noCheckAtlassianToken).toBe(true); + }); + + test("uses basic auth for cloud instances", async () => { + mockGetCurrentUser.mockResolvedValueOnce({ displayName: "User" }); + + await connector.testConnection({ + config: validConfig, + credentials, + }); + + const cloudConfig = capturedConfigs.find( + (c) => c.type === "Version3", + )?.config; + expect(cloudConfig?.authentication).toEqual({ + basic: { email: "user@example.com", apiToken: "test-api-token" }, + }); + }); }); describe("sync", () => { diff --git a/platform/backend/src/knowledge-base/connectors/jira/jira-connector.ts b/platform/backend/src/knowledge-base/connectors/jira/jira-connector.ts index d7c71cfeb1..d5227ac226 100644 --- a/platform/backend/src/knowledge-base/connectors/jira/jira-connector.ts +++ b/platform/backend/src/knowledge-base/connectors/jira/jira-connector.ts @@ -222,15 +222,12 @@ function createV2Client( config: JiraConfig, credentials: ConnectorCredentials, ): Version2Client { - // @ts-expect-error jira.js@5.3.1 overload resolution broken: private 'client' property intersects to 'never' return createClient(ClientType.Version2, { host: config.jiraBaseUrl.replace(/\/+$/, ""), - authentication: { - basic: { - email: credentials.email, - apiToken: credentials.apiToken, - }, - }, + noCheckAtlassianToken: true, + authentication: credentials.email + ? { basic: { email: credentials.email, apiToken: credentials.apiToken } } + : { oauth2: { accessToken: credentials.apiToken } }, }) as unknown as Version2Client; } diff --git a/platform/backend/src/models/index.ts b/platform/backend/src/models/index.ts index 629a3be796..c0f4ac794e 100644 --- a/platform/backend/src/models/index.ts +++ b/platform/backend/src/models/index.ts @@ -41,7 +41,10 @@ export { default as OAuthAccessTokenModel } from "./oauth-access-token"; export { default as OAuthClientModel } from "./oauth-client"; export { default as OptimizationRuleModel } from "./optimization-rule"; export { default as OrganizationModel } from "./organization"; -export { default as OrganizationRoleModel } from "./organization-role"; +export { + default as OrganizationRoleModel, + ROLE_SORT_COLUMNS, +} from "./organization-role"; export { default as ProcessedEmailModel } from "./processed-email"; export { default as SecretModel } from "./secret"; export { default as SessionModel } from "./session"; diff --git a/platform/backend/src/models/member.test.ts b/platform/backend/src/models/member.test.ts index e3d135777e..27857e5f7b 100644 --- a/platform/backend/src/models/member.test.ts +++ b/platform/backend/src/models/member.test.ts @@ -230,4 +230,387 @@ describe("MemberModel", () => { expect(updated?.role).toBe(customRole); }); }); + + describe("findAllPaginatedByOrganization", () => { + test("should return paginated members for organization", async ({ + makeUser, + makeOrganization, + makeMember, + }) => { + const org = await makeOrganization(); + const user1 = await makeUser({ name: "Alice" }); + const user2 = await makeUser({ name: "Bob" }); + const user3 = await makeUser({ name: "Charlie" }); + await makeMember(user1.id, org.id); + await makeMember(user2.id, org.id); + await makeMember(user3.id, org.id); + + const result = await MemberModel.findAllPaginatedByOrganization({ + organizationId: org.id, + pagination: { limit: 2, offset: 0 }, + sorting: { sortDirection: "asc" }, + }); + + expect(result.data).toHaveLength(2); + expect(result.pagination.total).toBe(3); + expect(result.pagination.hasNext).toBe(true); + expect(result.pagination.hasPrev).toBe(false); + }); + + test("should search members by name case-insensitively", async ({ + makeUser, + makeOrganization, + makeMember, + }) => { + const org = await makeOrganization(); + const user1 = await makeUser({ name: "Alice Johnson" }); + const user2 = await makeUser({ name: "Bob Smith" }); + const user3 = await makeUser({ name: "Charlie Johnson" }); + await makeMember(user1.id, org.id); + await makeMember(user2.id, org.id); + await makeMember(user3.id, org.id); + + const result = await MemberModel.findAllPaginatedByOrganization({ + organizationId: org.id, + pagination: { limit: 20, offset: 0 }, + sorting: {}, + search: "johnson", + }); + + expect(result.data).toHaveLength(2); + const names = result.data.map((m) => m.name); + expect(names).toContain("Alice Johnson"); + expect(names).toContain("Charlie Johnson"); + }); + + test("should search members by email case-insensitively", async ({ + makeUser, + makeOrganization, + makeMember, + }) => { + const org = await makeOrganization(); + const user1 = await makeUser({ email: "alice@example.com" }); + const user2 = await makeUser({ email: "bob@different.com" }); + await makeMember(user1.id, org.id); + await makeMember(user2.id, org.id); + + const result = await MemberModel.findAllPaginatedByOrganization({ + organizationId: org.id, + pagination: { limit: 20, offset: 0 }, + sorting: {}, + search: "EXAMPLE.COM", + }); + + expect(result.data).toHaveLength(1); + expect(result.data[0].email).toBe("alice@example.com"); + }); + + test("should filter members by role", async ({ + makeUser, + makeOrganization, + makeMember, + }) => { + const org = await makeOrganization(); + const admin = await makeUser({ name: "Admin User" }); + const member1 = await makeUser({ name: "Member One" }); + const member2 = await makeUser({ name: "Member Two" }); + await makeMember(admin.id, org.id, { role: ADMIN_ROLE_NAME }); + await makeMember(member1.id, org.id, { role: MEMBER_ROLE_NAME }); + await makeMember(member2.id, org.id, { role: MEMBER_ROLE_NAME }); + + const result = await MemberModel.findAllPaginatedByOrganization({ + organizationId: org.id, + pagination: { limit: 20, offset: 0 }, + sorting: {}, + role: ADMIN_ROLE_NAME, + }); + + expect(result.data).toHaveLength(1); + expect(result.data[0].role).toBe(ADMIN_ROLE_NAME); + }); + + test("should filter members by team", async ({ + makeUser, + makeOrganization, + makeMember, + makeTeam, + makeTeamMember, + }) => { + const org = await makeOrganization(); + const user1 = await makeUser({ name: "Alice" }); + const user2 = await makeUser({ name: "Bob" }); + const user3 = await makeUser({ name: "Charlie" }); + await makeMember(user1.id, org.id); + await makeMember(user2.id, org.id); + await makeMember(user3.id, org.id); + + const team = await makeTeam(org.id, user1.id, { + name: "Engineering", + }); + await makeTeamMember(team.id, user1.id); + await makeTeamMember(team.id, user2.id); + + const result = await MemberModel.findAllPaginatedByOrganization({ + organizationId: org.id, + pagination: { limit: 20, offset: 0 }, + sorting: {}, + teamIds: [team.id], + }); + + expect(result.data).toHaveLength(2); + const names = result.data.map((m) => m.name); + expect(names).toContain("Alice"); + expect(names).toContain("Bob"); + }); + + test("should sort members by name ascending", async ({ + makeUser, + makeOrganization, + makeMember, + }) => { + const org = await makeOrganization(); + const userC = await makeUser({ name: "Charlie" }); + const userA = await makeUser({ name: "Alice" }); + const userB = await makeUser({ name: "Bob" }); + await makeMember(userC.id, org.id); + await makeMember(userA.id, org.id); + await makeMember(userB.id, org.id); + + const result = await MemberModel.findAllPaginatedByOrganization({ + organizationId: org.id, + pagination: { limit: 20, offset: 0 }, + sorting: { sortBy: "name", sortDirection: "asc" }, + }); + + expect(result.data[0].name).toBe("Alice"); + expect(result.data[1].name).toBe("Bob"); + expect(result.data[2].name).toBe("Charlie"); + }); + + test("should sort members by email descending", async ({ + makeUser, + makeOrganization, + makeMember, + }) => { + const org = await makeOrganization(); + const userA = await makeUser({ email: "alice@test.com" }); + const userZ = await makeUser({ email: "zara@test.com" }); + const userM = await makeUser({ email: "mike@test.com" }); + await makeMember(userA.id, org.id); + await makeMember(userZ.id, org.id); + await makeMember(userM.id, org.id); + + const result = await MemberModel.findAllPaginatedByOrganization({ + organizationId: org.id, + pagination: { limit: 20, offset: 0 }, + sorting: { sortBy: "email", sortDirection: "desc" }, + }); + + expect(result.data[0].email).toBe("zara@test.com"); + expect(result.data[1].email).toBe("mike@test.com"); + expect(result.data[2].email).toBe("alice@test.com"); + }); + + test("should sort members by role", async ({ + makeUser, + makeOrganization, + makeMember, + }) => { + const org = await makeOrganization(); + const member = await makeUser({ name: "Member User" }); + const admin = await makeUser({ name: "Admin User" }); + await makeMember(member.id, org.id, { role: MEMBER_ROLE_NAME }); + await makeMember(admin.id, org.id, { role: ADMIN_ROLE_NAME }); + + const result = await MemberModel.findAllPaginatedByOrganization({ + organizationId: org.id, + pagination: { limit: 20, offset: 0 }, + sorting: { sortBy: "role", sortDirection: "asc" }, + }); + + expect(result.data[0].role).toBe(ADMIN_ROLE_NAME); + expect(result.data[1].role).toBe(MEMBER_ROLE_NAME); + }); + + test("should return correct team membership data", async ({ + makeUser, + makeOrganization, + makeMember, + makeTeam, + makeTeamMember, + }) => { + const org = await makeOrganization(); + const user = await makeUser({ name: "Alice" }); + await makeMember(user.id, org.id); + + const team1 = await makeTeam(org.id, user.id, { name: "Engineering" }); + const team2 = await makeTeam(org.id, user.id, { name: "Design" }); + await makeTeamMember(team1.id, user.id); + await makeTeamMember(team2.id, user.id); + + const result = await MemberModel.findAllPaginatedByOrganization({ + organizationId: org.id, + pagination: { limit: 20, offset: 0 }, + sorting: {}, + }); + + expect(result.data).toHaveLength(1); + expect(result.data[0].teams).toHaveLength(2); + const teamNames = result.data[0].teams.map((t) => t.name).sort(); + expect(teamNames).toEqual(["Design", "Engineering"]); + }); + + test("should return isPendingSignup true for users without accounts", async ({ + makeUser, + makeOrganization, + makeMember, + }) => { + const org = await makeOrganization(); + // makeUser creates a user WITHOUT an account record by default + const pendingUser = await makeUser({ name: "Pending User" }); + await makeMember(pendingUser.id, org.id); + + const result = await MemberModel.findAllPaginatedByOrganization({ + organizationId: org.id, + pagination: { limit: 20, offset: 0 }, + sorting: {}, + }); + + expect(result.data).toHaveLength(1); + expect(result.data[0].isPendingSignup).toBe(true); + }); + + test("should return isPendingSignup false for users with accounts", async ({ + makeUser, + makeOrganization, + makeMember, + makeAccount, + }) => { + const org = await makeOrganization(); + const activeUser = await makeUser({ name: "Active User" }); + await makeMember(activeUser.id, org.id); + await makeAccount(activeUser.id); + + const result = await MemberModel.findAllPaginatedByOrganization({ + organizationId: org.id, + pagination: { limit: 20, offset: 0 }, + sorting: {}, + }); + + expect(result.data).toHaveLength(1); + expect(result.data[0].isPendingSignup).toBe(false); + }); + + test("should handle second page correctly", async ({ + makeUser, + makeOrganization, + makeMember, + }) => { + const org = await makeOrganization(); + const user1 = await makeUser(); + const user2 = await makeUser(); + const user3 = await makeUser(); + await makeMember(user1.id, org.id); + await makeMember(user2.id, org.id); + await makeMember(user3.id, org.id); + + const result = await MemberModel.findAllPaginatedByOrganization({ + organizationId: org.id, + pagination: { limit: 2, offset: 2 }, + sorting: {}, + }); + + expect(result.data).toHaveLength(1); + expect(result.pagination.total).toBe(3); + expect(result.pagination.hasNext).toBe(false); + expect(result.pagination.hasPrev).toBe(true); + }); + + test("should not return members from other organizations", async ({ + makeUser, + makeOrganization, + makeMember, + }) => { + const org1 = await makeOrganization(); + const org2 = await makeOrganization(); + const user1 = await makeUser({ name: "Org1 User" }); + const user2 = await makeUser({ name: "Org2 User" }); + await makeMember(user1.id, org1.id); + await makeMember(user2.id, org2.id); + + const result = await MemberModel.findAllPaginatedByOrganization({ + organizationId: org1.id, + pagination: { limit: 20, offset: 0 }, + sorting: {}, + }); + + expect(result.data).toHaveLength(1); + expect(result.data[0].name).toBe("Org1 User"); + }); + + test("should return empty when search matches nothing", async ({ + makeUser, + makeOrganization, + makeMember, + }) => { + const org = await makeOrganization(); + const user = await makeUser({ name: "Alice" }); + await makeMember(user.id, org.id); + + const result = await MemberModel.findAllPaginatedByOrganization({ + organizationId: org.id, + pagination: { limit: 20, offset: 0 }, + sorting: {}, + search: "nonexistent", + }); + + expect(result.data).toHaveLength(0); + expect(result.pagination.total).toBe(0); + }); + + test("should return members with empty teams array when not in any team", async ({ + makeUser, + makeOrganization, + makeMember, + }) => { + const org = await makeOrganization(); + const user = await makeUser({ name: "Teamless User" }); + await makeMember(user.id, org.id); + + const result = await MemberModel.findAllPaginatedByOrganization({ + organizationId: org.id, + pagination: { limit: 20, offset: 0 }, + sorting: {}, + }); + + expect(result.data).toHaveLength(1); + expect(result.data[0].teams).toEqual([]); + }); + + test("should combine search and role filter", async ({ + makeUser, + makeOrganization, + makeMember, + }) => { + const org = await makeOrganization(); + const adminAlice = await makeUser({ name: "Alice Admin" }); + const memberAlice = await makeUser({ name: "Alice Member" }); + const adminBob = await makeUser({ name: "Bob Admin" }); + await makeMember(adminAlice.id, org.id, { role: ADMIN_ROLE_NAME }); + await makeMember(memberAlice.id, org.id, { role: MEMBER_ROLE_NAME }); + await makeMember(adminBob.id, org.id, { role: ADMIN_ROLE_NAME }); + + const result = await MemberModel.findAllPaginatedByOrganization({ + organizationId: org.id, + pagination: { limit: 20, offset: 0 }, + sorting: {}, + search: "Alice", + role: ADMIN_ROLE_NAME, + }); + + expect(result.data).toHaveLength(1); + expect(result.data[0].name).toBe("Alice Admin"); + expect(result.data[0].role).toBe(ADMIN_ROLE_NAME); + }); + }); }); diff --git a/platform/backend/src/models/member.ts b/platform/backend/src/models/member.ts index 537af2c684..4b8bfc1820 100644 --- a/platform/backend/src/models/member.ts +++ b/platform/backend/src/models/member.ts @@ -1,7 +1,28 @@ import type { AnyRoleName } from "@shared"; -import { and, count, eq } from "drizzle-orm"; +import { + and, + asc, + count, + desc, + eq, + ilike, + inArray, + or, + type SQL, +} from "drizzle-orm"; import db, { schema } from "@/database"; +import type { PaginatedResult } from "@/database/utils/pagination"; +import { createPaginatedResult } from "@/database/utils/pagination"; import logger from "@/logging"; +import type { PaginationQuery, SortingQueryFor } from "@/types"; +import type { MemberWithUser } from "@/types/member"; + +export const MEMBER_SORT_COLUMNS = [ + "name", + "email", + "role", + "createdAt", +] as const; class MemberModel { /** @@ -273,6 +294,166 @@ class MemberModel { .where(eq(schema.membersTable.defaultAgentId, agentId)); return (result?.count ?? 0) > 0; } + + /** + * Find all members with user details, pagination, sorting, search, and filtering. + * Includes pending signup status and team memberships (batch-loaded). + */ + static async findAllPaginatedByOrganization(params: { + organizationId: string; + pagination: PaginationQuery; + sorting: SortingQueryFor; + search?: string; + teamIds?: string[]; + role?: string; + }): Promise> { + const { organizationId, pagination, sorting, search, teamIds, role } = + params; + + logger.debug( + { organizationId, pagination, sorting, search, teamIds, role }, + "MemberModel.findAllPaginatedByOrganization: fetching members", + ); + + // Build WHERE conditions + const conditions: SQL[] = [ + eq(schema.membersTable.organizationId, organizationId), + ]; + + // Search across user name and email + if (search) { + const searchPattern = `%${search}%`; + const searchCondition = or( + ilike(schema.usersTable.name, searchPattern), + ilike(schema.usersTable.email, searchPattern), + ); + if (searchCondition) { + conditions.push(searchCondition); + } + } + + // Filter by role + if (role) { + conditions.push(eq(schema.membersTable.role, role)); + } + + // Filter by team membership: members who belong to any of the specified teams + if (teamIds && teamIds.length > 0) { + const membersInTeams = db + .select({ userId: schema.teamMembersTable.userId }) + .from(schema.teamMembersTable) + .where(inArray(schema.teamMembersTable.teamId, teamIds)); + conditions.push(inArray(schema.membersTable.userId, membersInTeams)); + } + + const whereClause = and(...conditions); + + // Determine sort column and direction + const direction = sorting.sortDirection === "asc" ? asc : desc; + const sortColumn = (() => { + switch (sorting.sortBy) { + case "name": + return schema.usersTable.name; + case "email": + return schema.usersTable.email; + case "role": + return schema.membersTable.role; + case "createdAt": + return schema.membersTable.createdAt; + default: + return schema.membersTable.createdAt; + } + })(); + const orderByClause = direction(sortColumn); + + // Fetch paginated data and total count in parallel + const [rows, [{ total }]] = await Promise.all([ + db + .select({ + id: schema.membersTable.id, + userId: schema.membersTable.userId, + name: schema.usersTable.name, + email: schema.usersTable.email, + role: schema.membersTable.role, + createdAt: schema.membersTable.createdAt, + }) + .from(schema.membersTable) + .innerJoin( + schema.usersTable, + eq(schema.membersTable.userId, schema.usersTable.id), + ) + .where(whereClause) + .orderBy(orderByClause) + .limit(pagination.limit) + .offset(pagination.offset), + db + .select({ total: count() }) + .from(schema.membersTable) + .innerJoin( + schema.usersTable, + eq(schema.membersTable.userId, schema.usersTable.id), + ) + .where(whereClause), + ]); + + if (rows.length === 0) { + return createPaginatedResult([], Number(total), pagination); + } + + const userIds = rows.map((r) => r.userId); + + // Batch-load team memberships for all returned members + const teamMemberships = await db + .select({ + userId: schema.teamMembersTable.userId, + teamId: schema.teamMembersTable.teamId, + teamName: schema.teamsTable.name, + }) + .from(schema.teamMembersTable) + .innerJoin( + schema.teamsTable, + eq(schema.teamMembersTable.teamId, schema.teamsTable.id), + ) + .where(inArray(schema.teamMembersTable.userId, userIds)); + + const teamsByUserId = new Map< + string, + Array<{ id: string; name: string }> + >(); + for (const userId of userIds) { + teamsByUserId.set(userId, []); + } + for (const tm of teamMemberships) { + const teams = teamsByUserId.get(tm.userId) || []; + teams.push({ id: tm.teamId, name: tm.teamName }); + teamsByUserId.set(tm.userId, teams); + } + + // Batch-load pending signup status: users without an account record + const usersWithAccounts = await db + .select({ userId: schema.accountsTable.userId }) + .from(schema.accountsTable) + .where(inArray(schema.accountsTable.userId, userIds)); + const hasAccountSet = new Set(usersWithAccounts.map((a) => a.userId)); + + const data: MemberWithUser[] = rows.map((row) => ({ + id: row.id, + userId: row.userId, + name: row.name, + email: row.email, + role: row.role, + createdAt: row.createdAt, + teams: teamsByUserId.get(row.userId) || [], + isPendingSignup: !hasAccountSet.has(row.userId), + })); + + logger.debug( + { organizationId, count: data.length, total: Number(total) }, + "MemberModel.findAllPaginatedByOrganization: completed", + ); + + return createPaginatedResult(data, Number(total), pagination); + } } export default MemberModel; diff --git a/platform/backend/src/models/organization-role.test.ts b/platform/backend/src/models/organization-role.test.ts index d955303f9d..a560d610df 100644 --- a/platform/backend/src/models/organization-role.test.ts +++ b/platform/backend/src/models/organization-role.test.ts @@ -253,6 +253,273 @@ describe("OrganizationRoleModel", () => { }); }); + describe("getAllPaginated", () => { + test("should return predefined roles first, then custom roles", async ({ + makeOrganization, + makeCustomRole, + }) => { + const org = await makeOrganization(); + await makeCustomRole(org.id, { + role: "custom_role", + name: "Custom Role", + permission: { agent: ["read"] }, + }); + + const result = await OrganizationRoleModel.getAllPaginated({ + organizationId: org.id, + pagination: { limit: 20, offset: 0 }, + sorting: {}, + }); + + // 3 predefined + 1 custom + expect(result.data).toHaveLength(4); + expect(result.pagination.total).toBe(4); + + // First 3 should be predefined (admin, editor, member) + expect(result.data[0].predefined).toBe(true); + expect(result.data[0].name).toBe(ADMIN_ROLE_NAME); + expect(result.data[1].predefined).toBe(true); + expect(result.data[1].name).toBe(EDITOR_ROLE_NAME); + expect(result.data[2].predefined).toBe(true); + expect(result.data[2].name).toBe(MEMBER_ROLE_NAME); + + // Last should be custom + expect(result.data[3].predefined).toBe(false); + expect(result.data[3].name).toBe("Custom Role"); + }); + + test("should paginate across predefined and custom roles", async ({ + makeOrganization, + makeCustomRole, + }) => { + const org = await makeOrganization(); + await makeCustomRole(org.id, { + role: "custom_1", + name: "Custom One", + permission: { agent: ["read"] }, + }); + await makeCustomRole(org.id, { + role: "custom_2", + name: "Custom Two", + permission: { agent: ["create"] }, + }); + + // Page 1: should get first 3 (predefined roles) + const page1 = await OrganizationRoleModel.getAllPaginated({ + organizationId: org.id, + pagination: { limit: 3, offset: 0 }, + sorting: {}, + }); + + expect(page1.data).toHaveLength(3); + expect(page1.pagination.total).toBe(5); + expect(page1.pagination.hasNext).toBe(true); + expect(page1.pagination.hasPrev).toBe(false); + expect(page1.data.every((r) => r.predefined)).toBe(true); + + // Page 2: should get 2 custom roles + const page2 = await OrganizationRoleModel.getAllPaginated({ + organizationId: org.id, + pagination: { limit: 3, offset: 3 }, + sorting: {}, + }); + + expect(page2.data).toHaveLength(2); + expect(page2.pagination.hasNext).toBe(false); + expect(page2.pagination.hasPrev).toBe(true); + expect(page2.data.every((r) => !r.predefined)).toBe(true); + }); + + test("should filter predefined roles by search", async ({ + makeOrganization, + }) => { + const org = await makeOrganization(); + + const result = await OrganizationRoleModel.getAllPaginated({ + organizationId: org.id, + pagination: { limit: 20, offset: 0 }, + sorting: {}, + search: "admin", + }); + + expect(result.data).toHaveLength(1); + expect(result.data[0].name).toBe(ADMIN_ROLE_NAME); + expect(result.data[0].predefined).toBe(true); + }); + + test("should filter custom roles by search", async ({ + makeOrganization, + makeCustomRole, + }) => { + const org = await makeOrganization(); + await makeCustomRole(org.id, { + role: "viewer_role", + name: "Viewer", + permission: { agent: ["read"] }, + }); + await makeCustomRole(org.id, { + role: "editor_custom", + name: "Custom Editor", + permission: { agent: ["read", "update"] }, + }); + + const result = await OrganizationRoleModel.getAllPaginated({ + organizationId: org.id, + pagination: { limit: 20, offset: 0 }, + sorting: {}, + search: "viewer", + }); + + // Should only match the custom "Viewer" role (not predefined ones) + expect(result.data).toHaveLength(1); + expect(result.data[0].name).toBe("Viewer"); + }); + + test("should sort custom roles by name ascending", async ({ + makeOrganization, + makeCustomRole, + }) => { + const org = await makeOrganization(); + await makeCustomRole(org.id, { + role: "zebra_role", + name: "Zebra Role", + permission: { agent: ["read"] }, + }); + await makeCustomRole(org.id, { + role: "alpha_role", + name: "Alpha Role", + permission: { agent: ["read"] }, + }); + + const result = await OrganizationRoleModel.getAllPaginated({ + organizationId: org.id, + pagination: { limit: 20, offset: 0 }, + sorting: { sortBy: "name", sortDirection: "asc" }, + }); + + // Predefined first, then custom sorted by name asc + const customRoles = result.data.filter((r) => !r.predefined); + expect(customRoles).toHaveLength(2); + expect(customRoles[0].name).toBe("Alpha Role"); + expect(customRoles[1].name).toBe("Zebra Role"); + }); + + test("should sort custom roles by name descending", async ({ + makeOrganization, + makeCustomRole, + }) => { + const org = await makeOrganization(); + await makeCustomRole(org.id, { + role: "zebra_role", + name: "Zebra Role", + permission: { agent: ["read"] }, + }); + await makeCustomRole(org.id, { + role: "alpha_role", + name: "Alpha Role", + permission: { agent: ["read"] }, + }); + + const result = await OrganizationRoleModel.getAllPaginated({ + organizationId: org.id, + pagination: { limit: 20, offset: 0 }, + sorting: { sortBy: "name", sortDirection: "desc" }, + }); + + const customRoles = result.data.filter((r) => !r.predefined); + expect(customRoles).toHaveLength(2); + expect(customRoles[0].name).toBe("Zebra Role"); + expect(customRoles[1].name).toBe("Alpha Role"); + }); + + test("should return only predefined roles when no custom roles exist", async ({ + makeOrganization, + }) => { + const org = await makeOrganization(); + + const result = await OrganizationRoleModel.getAllPaginated({ + organizationId: org.id, + pagination: { limit: 20, offset: 0 }, + sorting: {}, + }); + + expect(result.data).toHaveLength(3); + expect(result.pagination.total).toBe(3); + expect(result.data.every((r) => r.predefined)).toBe(true); + }); + + test("should return empty when search matches nothing", async ({ + makeOrganization, + }) => { + const org = await makeOrganization(); + + const result = await OrganizationRoleModel.getAllPaginated({ + organizationId: org.id, + pagination: { limit: 20, offset: 0 }, + sorting: {}, + search: "nonexistent", + }); + + expect(result.data).toHaveLength(0); + expect(result.pagination.total).toBe(0); + }); + + test("should not return custom roles from other organizations", async ({ + makeOrganization, + makeCustomRole, + }) => { + const org1 = await makeOrganization(); + const org2 = await makeOrganization(); + await makeCustomRole(org1.id, { + role: "org1_role", + name: "Org1 Custom Role", + permission: { agent: ["read"] }, + }); + await makeCustomRole(org2.id, { + role: "org2_role", + name: "Org2 Custom Role", + permission: { agent: ["read"] }, + }); + + const result = await OrganizationRoleModel.getAllPaginated({ + organizationId: org1.id, + pagination: { limit: 20, offset: 0 }, + sorting: {}, + }); + + // 3 predefined + 1 custom from org1 + expect(result.data).toHaveLength(4); + const customRoles = result.data.filter((r) => !r.predefined); + expect(customRoles).toHaveLength(1); + expect(customRoles[0].name).toBe("Org1 Custom Role"); + }); + + test("should search across both predefined and custom roles simultaneously", async ({ + makeOrganization, + makeCustomRole, + }) => { + const org = await makeOrganization(); + await makeCustomRole(org.id, { + role: "member_extended", + name: "Member Extended", + permission: { agent: ["read", "create"] }, + }); + + const result = await OrganizationRoleModel.getAllPaginated({ + organizationId: org.id, + pagination: { limit: 20, offset: 0 }, + sorting: {}, + search: "member", + }); + + // Should match predefined "member" and custom "Member Extended" + expect(result.data).toHaveLength(2); + const names = result.data.map((r) => r.name); + expect(names).toContain(MEMBER_ROLE_NAME); + expect(names).toContain("Member Extended"); + }); + }); + describe("canDelete", () => { test("should return false for predefined roles", async ({ makeOrganization, diff --git a/platform/backend/src/models/organization-role.ts b/platform/backend/src/models/organization-role.ts index 3e08ac82ca..1941df3d05 100644 --- a/platform/backend/src/models/organization-role.ts +++ b/platform/backend/src/models/organization-role.ts @@ -8,10 +8,20 @@ import { type Resource, } from "@shared"; import { predefinedPermissionsMap } from "@shared/access-control"; -import { and, eq, getTableColumns, sql } from "drizzle-orm"; +import { and, count, eq, getTableColumns, ilike, or, sql } from "drizzle-orm"; import db, { schema } from "@/database"; +import { + createPaginatedResult, + type PaginatedResult, +} from "@/database/utils/pagination"; import logger from "@/logging"; -import type { OrganizationRole } from "@/types"; +import type { + OrganizationRole, + PaginationQuery, + SortingQueryFor, +} from "@/types"; + +export const ROLE_SORT_COLUMNS = ["name", "createdAt"] as const; const generatePredefinedRole = ( role: PredefinedRoleName, @@ -20,6 +30,7 @@ const generatePredefinedRole = ( id: role, role: role, name: role, + description: null, organizationId, permission: OrganizationRoleModel.getPredefinedRolePermissions(role), predefined: true, @@ -390,6 +401,92 @@ class OrganizationRoleModel { } } + /** + * List all roles for an organization with pagination, sorting, and search. + * Predefined roles always appear first, before custom roles. + */ + static async getAllPaginated(params: { + organizationId: string; + pagination: PaginationQuery; + sorting: SortingQueryFor; + search?: string; + }): Promise> { + const { organizationId, pagination, sorting, search } = params; + + // Get predefined roles, filtered by search if applicable + let predefinedRoles = + OrganizationRoleModel.getPredefinedOnly(organizationId); + + if (search) { + const searchLower = search.toLowerCase(); + predefinedRoles = predefinedRoles.filter( + (r) => + r.name.toLowerCase().includes(searchLower) || + (r.description?.toLowerCase().includes(searchLower) ?? false), + ); + } + + // Build where conditions for custom roles query + const conditions = [ + eq(schema.organizationRolesTable.organizationId, organizationId), + ]; + if (search) { + const searchCondition = or( + ilike(schema.organizationRolesTable.name, `%${search}%`), + ilike(schema.organizationRolesTable.description, `%${search}%`), + ); + if (searchCondition) { + conditions.push(searchCondition); + } + } + + const whereClause = and(...conditions); + + // Fetch all matching custom roles and count in parallel + const [customRolesRaw, [{ total: customTotal }]] = await Promise.all([ + db + .select({ + ...getTableColumns(schema.organizationRolesTable), + predefined: sql`false`, + }) + .from(schema.organizationRolesTable) + .where(whereClause), + db + .select({ total: count() }) + .from(schema.organizationRolesTable) + .where(whereClause), + ]); + + const customRoles: OrganizationRole[] = customRolesRaw.map((role) => ({ + ...role, + permission: JSON.parse(role.permission), + })); + + // Sort custom roles based on sorting params + const sortDirection = sorting.sortDirection === "asc" ? 1 : -1; + customRoles.sort((a, b) => { + if (sorting.sortBy === "name") { + return a.name.localeCompare(b.name) * sortDirection; + } + // Default sort by createdAt + const aTime = a.createdAt.getTime(); + const bTime = b.createdAt.getTime(); + return (aTime - bTime) * sortDirection; + }); + + // Combine: predefined first, then sorted custom roles + const allRoles = [...predefinedRoles, ...customRoles]; + const total = predefinedRoles.length + Number(customTotal); + + // Apply pagination in memory across the combined list + const paginatedRoles = allRoles.slice( + pagination.offset, + pagination.offset + pagination.limit, + ); + + return createPaginatedResult(paginatedRoles, total, pagination); + } + /** * @deprecated Do not use directly. Routes should use betterAuth.api.createOrgRole() instead. * This method exists only for test fixtures. diff --git a/platform/backend/src/models/team.test.ts b/platform/backend/src/models/team.test.ts index ca1ecd10b5..6ef235f0e1 100644 --- a/platform/backend/src/models/team.test.ts +++ b/platform/backend/src/models/team.test.ts @@ -108,6 +108,328 @@ describe("TeamModel", () => { }); }); + describe("findAllPaginated", () => { + test("should return paginated teams for organization", async ({ + makeOrganization, + makeUser, + makeTeam, + }) => { + const org = await makeOrganization(); + const user = await makeUser(); + await makeTeam(org.id, user.id, { name: "Alpha Team" }); + await makeTeam(org.id, user.id, { name: "Beta Team" }); + await makeTeam(org.id, user.id, { name: "Gamma Team" }); + + const result = await TeamModel.findAllPaginated({ + organizationId: org.id, + pagination: { limit: 2, offset: 0 }, + sorting: { sortDirection: "asc" }, + isTeamAdmin: true, + }); + + expect(result.data).toHaveLength(2); + expect(result.pagination.total).toBe(3); + expect(result.pagination.hasNext).toBe(true); + expect(result.pagination.hasPrev).toBe(false); + }); + + test("should search teams by name case-insensitively", async ({ + makeOrganization, + makeUser, + makeTeam, + }) => { + const org = await makeOrganization(); + const user = await makeUser(); + await makeTeam(org.id, user.id, { name: "Engineering Team" }); + await makeTeam(org.id, user.id, { name: "Marketing Team" }); + await makeTeam(org.id, user.id, { name: "Sales Team" }); + + const result = await TeamModel.findAllPaginated({ + organizationId: org.id, + pagination: { limit: 20, offset: 0 }, + sorting: {}, + search: "engineering", + isTeamAdmin: true, + }); + + expect(result.data).toHaveLength(1); + expect(result.data[0].name).toBe("Engineering Team"); + }); + + test("should search teams by description", async ({ + makeOrganization, + makeUser, + makeTeam, + }) => { + const org = await makeOrganization(); + const user = await makeUser(); + await makeTeam(org.id, user.id, { + name: "Team A", + description: "Handles frontend development", + }); + await makeTeam(org.id, user.id, { + name: "Team B", + description: "Handles backend development", + }); + await makeTeam(org.id, user.id, { + name: "Team C", + description: "Handles sales", + }); + + const result = await TeamModel.findAllPaginated({ + organizationId: org.id, + pagination: { limit: 20, offset: 0 }, + sorting: {}, + search: "development", + isTeamAdmin: true, + }); + + expect(result.data).toHaveLength(2); + }); + + test("should sort teams by name ascending", async ({ + makeOrganization, + makeUser, + makeTeam, + }) => { + const org = await makeOrganization(); + const user = await makeUser(); + await makeTeam(org.id, user.id, { name: "Zebra Team" }); + await makeTeam(org.id, user.id, { name: "Alpha Team" }); + await makeTeam(org.id, user.id, { name: "Middle Team" }); + + const result = await TeamModel.findAllPaginated({ + organizationId: org.id, + pagination: { limit: 20, offset: 0 }, + sorting: { sortBy: "name", sortDirection: "asc" }, + isTeamAdmin: true, + }); + + expect(result.data[0].name).toBe("Alpha Team"); + expect(result.data[1].name).toBe("Middle Team"); + expect(result.data[2].name).toBe("Zebra Team"); + }); + + test("should sort teams by name descending", async ({ + makeOrganization, + makeUser, + makeTeam, + }) => { + const org = await makeOrganization(); + const user = await makeUser(); + await makeTeam(org.id, user.id, { name: "Zebra Team" }); + await makeTeam(org.id, user.id, { name: "Alpha Team" }); + await makeTeam(org.id, user.id, { name: "Middle Team" }); + + const result = await TeamModel.findAllPaginated({ + organizationId: org.id, + pagination: { limit: 20, offset: 0 }, + sorting: { sortBy: "name", sortDirection: "desc" }, + isTeamAdmin: true, + }); + + expect(result.data[0].name).toBe("Zebra Team"); + expect(result.data[1].name).toBe("Middle Team"); + expect(result.data[2].name).toBe("Alpha Team"); + }); + + test("should only return user's teams when not admin", async ({ + makeOrganization, + makeUser, + makeTeam, + makeTeamMember, + }) => { + const org = await makeOrganization(); + const user = await makeUser(); + const otherUser = await makeUser(); + + const team1 = await makeTeam(org.id, user.id, { name: "User Team" }); + await makeTeam(org.id, user.id, { name: "Other Team" }); + + // Add otherUser to team1 only + await makeTeamMember(team1.id, otherUser.id); + + const result = await TeamModel.findAllPaginated({ + organizationId: org.id, + pagination: { limit: 20, offset: 0 }, + sorting: {}, + userId: otherUser.id, + isTeamAdmin: false, + }); + + expect(result.data).toHaveLength(1); + expect(result.data[0].name).toBe("User Team"); + }); + + test("should return empty result for non-admin user with no teams", async ({ + makeOrganization, + makeUser, + makeTeam, + }) => { + const org = await makeOrganization(); + const user = await makeUser(); + const otherUser = await makeUser(); + await makeTeam(org.id, user.id, { name: "Some Team" }); + + const result = await TeamModel.findAllPaginated({ + organizationId: org.id, + pagination: { limit: 20, offset: 0 }, + sorting: {}, + userId: otherUser.id, + isTeamAdmin: false, + }); + + expect(result.data).toHaveLength(0); + expect(result.pagination.total).toBe(0); + }); + + test("should include members in paginated results", async ({ + makeOrganization, + makeUser, + makeTeam, + makeTeamMember, + }) => { + const org = await makeOrganization(); + const user1 = await makeUser(); + const user2 = await makeUser(); + const team = await makeTeam(org.id, user1.id, { name: "Test Team" }); + await makeTeamMember(team.id, user1.id); + await makeTeamMember(team.id, user2.id); + + const result = await TeamModel.findAllPaginated({ + organizationId: org.id, + pagination: { limit: 20, offset: 0 }, + sorting: {}, + isTeamAdmin: true, + }); + + expect(result.data).toHaveLength(1); + expect(result.data[0].members).toHaveLength(2); + }); + + test("should handle second page correctly", async ({ + makeOrganization, + makeUser, + makeTeam, + }) => { + const org = await makeOrganization(); + const user = await makeUser(); + await makeTeam(org.id, user.id, { name: "Team 1" }); + await makeTeam(org.id, user.id, { name: "Team 2" }); + await makeTeam(org.id, user.id, { name: "Team 3" }); + + const result = await TeamModel.findAllPaginated({ + organizationId: org.id, + pagination: { limit: 2, offset: 2 }, + sorting: {}, + isTeamAdmin: true, + }); + + expect(result.data).toHaveLength(1); + expect(result.pagination.total).toBe(3); + expect(result.pagination.hasNext).toBe(false); + expect(result.pagination.hasPrev).toBe(true); + }); + + test("should not return teams from other organizations", async ({ + makeOrganization, + makeUser, + makeTeam, + }) => { + const org1 = await makeOrganization(); + const org2 = await makeOrganization(); + const user = await makeUser(); + await makeTeam(org1.id, user.id, { name: "Org1 Team" }); + await makeTeam(org2.id, user.id, { name: "Org2 Team" }); + + const result = await TeamModel.findAllPaginated({ + organizationId: org1.id, + pagination: { limit: 20, offset: 0 }, + sorting: {}, + isTeamAdmin: true, + }); + + expect(result.data).toHaveLength(1); + expect(result.data[0].name).toBe("Org1 Team"); + }); + + test("should sort teams by memberCount", async ({ + makeOrganization, + makeUser, + makeTeam, + makeTeamMember, + }) => { + const org = await makeOrganization(); + const user1 = await makeUser(); + const user2 = await makeUser(); + const user3 = await makeUser(); + + const teamA = await makeTeam(org.id, user1.id, { name: "Small Team" }); + const teamB = await makeTeam(org.id, user1.id, { name: "Big Team" }); + await makeTeam(org.id, user1.id, { name: "Empty Team" }); + + await makeTeamMember(teamA.id, user1.id); + await makeTeamMember(teamB.id, user1.id); + await makeTeamMember(teamB.id, user2.id); + await makeTeamMember(teamB.id, user3.id); + + const result = await TeamModel.findAllPaginated({ + organizationId: org.id, + pagination: { limit: 20, offset: 0 }, + sorting: { sortBy: "memberCount", sortDirection: "desc" }, + isTeamAdmin: true, + }); + + expect(result.data[0].name).toBe("Big Team"); + expect(result.data[0].members).toHaveLength(3); + expect(result.data[1].name).toBe("Small Team"); + expect(result.data[1].members).toHaveLength(1); + expect(result.data[2].name).toBe("Empty Team"); + expect(result.data[2].members).toHaveLength(0); + }); + + test("should return all teams for admin regardless of membership", async ({ + makeOrganization, + makeUser, + makeTeam, + }) => { + const org = await makeOrganization(); + const creator = await makeUser(); + await makeTeam(org.id, creator.id, { name: "Team A" }); + await makeTeam(org.id, creator.id, { name: "Team B" }); + + const result = await TeamModel.findAllPaginated({ + organizationId: org.id, + pagination: { limit: 20, offset: 0 }, + sorting: {}, + isTeamAdmin: true, + }); + + expect(result.data).toHaveLength(2); + }); + + test("should return empty when search matches nothing", async ({ + makeOrganization, + makeUser, + makeTeam, + }) => { + const org = await makeOrganization(); + const user = await makeUser(); + await makeTeam(org.id, user.id, { name: "Alpha Team" }); + + const result = await TeamModel.findAllPaginated({ + organizationId: org.id, + pagination: { limit: 20, offset: 0 }, + sorting: {}, + search: "nonexistent", + isTeamAdmin: true, + }); + + expect(result.data).toHaveLength(0); + expect(result.pagination.total).toBe(0); + }); + }); + describe("getUserTeams", () => { test("returns teams with members for the user", async ({ makeUser, @@ -241,7 +563,7 @@ describe("TeamModel", () => { const org = await makeOrganization(); const team1 = await makeTeam(org.id, user.id, { name: "Team 1" }); const team2 = await makeTeam(org.id, user.id, { name: "Team 2" }); - const _team3 = await makeTeam(org.id, user.id, { name: "Team 3" }); + await makeTeam(org.id, user.id, { name: "Team 3" }); const teams = await TeamModel.findByIds([team1.id, team2.id]); @@ -840,7 +1162,7 @@ describe("TeamModel", () => { const org = await makeOrganization(); const team1 = await makeTeam(org.id, user.id, { name: "Team 1" }); const team2 = await makeTeam(org.id, user.id, { name: "Team 2" }); - const _team3 = await makeTeam(org.id, user.id, { name: "Team 3" }); + await makeTeam(org.id, user.id, { name: "Team 3" }); // Add user to team1 and team2, but not team3 await TeamModel.addMember(team1.id, user.id); diff --git a/platform/backend/src/models/team.ts b/platform/backend/src/models/team.ts index 864465d6bc..45ea403aba 100644 --- a/platform/backend/src/models/team.ts +++ b/platform/backend/src/models/team.ts @@ -1,9 +1,25 @@ import { MEMBER_ROLE_NAME } from "@shared"; -import { and, eq, inArray } from "drizzle-orm"; +import { + and, + asc, + count, + desc, + eq, + ilike, + inArray, + or, + sql, +} from "drizzle-orm"; import db, { schema } from "@/database"; +import { + createPaginatedResult, + type PaginatedResult, +} from "@/database/utils/pagination"; import logger from "@/logging"; import type { InsertTeam, + PaginationQuery, + SortingQueryFor, Team, TeamExternalGroup, TeamMember, @@ -12,6 +28,8 @@ import type { import { ApiError } from "@/types"; import TeamTokenModel from "./team-token"; +export const TEAM_SORT_COLUMNS = ["name", "createdAt", "memberCount"] as const; + class TeamModel { /** * Create a new team @@ -79,6 +97,113 @@ class TeamModel { return teamsWithMembers; } + /** + * Find all teams in an organization with pagination, search, and sorting + */ + static async findAllPaginated(params: { + organizationId: string; + pagination: PaginationQuery; + sorting: SortingQueryFor; + search?: string; + userId?: string; + isTeamAdmin: boolean; + }): Promise> { + const { organizationId, pagination, sorting, search, userId, isTeamAdmin } = + params; + + logger.debug( + { organizationId, pagination, sorting, search, isTeamAdmin }, + "TeamModel.findAllPaginated: fetching teams", + ); + + // Build WHERE conditions + const conditions = [eq(schema.teamsTable.organizationId, organizationId)]; + + // Add search filter + if (search) { + const searchCondition = or( + ilike(schema.teamsTable.name, `%${search}%`), + ilike(schema.teamsTable.description, `%${search}%`), + ); + if (searchCondition) { + conditions.push(searchCondition); + } + } + + // For non-admin users, only return teams where user is a member + if (!isTeamAdmin && userId) { + const userTeamIds = await TeamModel.getUserTeamIds(userId); + if (userTeamIds.length === 0) { + return createPaginatedResult([], 0, pagination); + } + conditions.push(inArray(schema.teamsTable.id, userTeamIds)); + } + + const whereClause = and(...conditions); + + // Build member count subquery for sorting by memberCount + const memberCountSubquery = db + .select({ + teamId: schema.teamMembersTable.teamId, + memberCount: count().as("member_count"), + }) + .from(schema.teamMembersTable) + .groupBy(schema.teamMembersTable.teamId) + .as("member_counts"); + + // Determine sort column and direction + const direction = sorting.sortDirection === "asc" ? asc : desc; + const sortColumn = + sorting.sortBy === "memberCount" + ? sql`COALESCE(${memberCountSubquery.memberCount}, 0)` + : sorting.sortBy === "name" + ? schema.teamsTable.name + : schema.teamsTable.createdAt; + + // Get total count + const [{ total }] = await db + .select({ total: count() }) + .from(schema.teamsTable) + .where(whereClause); + + // Get paginated results with sorting + const teams = await db + .select({ + id: schema.teamsTable.id, + name: schema.teamsTable.name, + description: schema.teamsTable.description, + organizationId: schema.teamsTable.organizationId, + createdBy: schema.teamsTable.createdBy, + createdAt: schema.teamsTable.createdAt, + updatedAt: schema.teamsTable.updatedAt, + convertToolResultsToToon: schema.teamsTable.convertToolResultsToToon, + }) + .from(schema.teamsTable) + .leftJoin( + memberCountSubquery, + eq(schema.teamsTable.id, memberCountSubquery.teamId), + ) + .where(whereClause) + .orderBy(direction(sortColumn)) + .limit(pagination.limit) + .offset(pagination.offset); + + // Batch fetch members for the paginated teams + const teamIds = teams.map((t) => t.id); + const membersByTeam = await TeamModel.getTeamMembersBatch(teamIds); + + const teamsWithMembers: Team[] = teams.map((team) => ({ + ...team, + members: membersByTeam.get(team.id) || [], + })); + + logger.debug( + { organizationId, total: Number(total), count: teamsWithMembers.length }, + "TeamModel.findAllPaginated: completed", + ); + return createPaginatedResult(teamsWithMembers, Number(total), pagination); + } + /** * Find a team by name within an organization */ diff --git a/platform/backend/src/routes/custom-role.ee.ts b/platform/backend/src/routes/custom-role.ee.ts index fa9aa408fc..559124b118 100644 --- a/platform/backend/src/routes/custom-role.ee.ts +++ b/platform/backend/src/routes/custom-role.ee.ts @@ -48,13 +48,14 @@ const customRoleRoutes: FastifyPluginAsyncZod = async (fastify) => { tags: ["Roles"], body: z.object({ name: CreateUpdateRoleNameSchema, + description: z.string().max(500).nullish(), permission: PermissionsSchema, }), response: constructResponseSchema(SelectOrganizationRoleSchema), }, }, async (request, reply) => { - const { name, permission } = request.body; + const { name, description, permission } = request.body; const { organizationId, user } = request; // Get user's permissions to validate they can grant these permissions @@ -95,6 +96,7 @@ const customRoleRoutes: FastifyPluginAsyncZod = async (fastify) => { permission, additionalFields: { name, + description: description ?? null, }, organizationId, }, @@ -138,6 +140,7 @@ const customRoleRoutes: FastifyPluginAsyncZod = async (fastify) => { }), body: z.object({ name: CreateUpdateRoleNameSchema.optional(), + description: z.string().max(500).nullish(), permission: PermissionsSchema.optional(), }), response: constructResponseSchema(SelectOrganizationRoleSchema), @@ -146,7 +149,7 @@ const customRoleRoutes: FastifyPluginAsyncZod = async (fastify) => { async ( { params: { roleId }, - body: { name, permission }, + body: { name, description, permission }, user, organizationId, headers, @@ -191,6 +194,8 @@ const customRoleRoutes: FastifyPluginAsyncZod = async (fastify) => { // Build update data const updateData: Record = {}; if (name) updateData.name = name; + if (description !== undefined) + updateData.description = description ?? null; if (permission) updateData.permission = permission; const result = await betterAuth.api.updateOrgRole({ diff --git a/platform/backend/src/routes/organization-role.ts b/platform/backend/src/routes/organization-role.ts index 8e4207fdf6..9d1154450d 100644 --- a/platform/backend/src/routes/organization-role.ts +++ b/platform/backend/src/routes/organization-role.ts @@ -2,10 +2,14 @@ import { PredefinedRoleNameSchema, RouteId } from "@shared"; import type { FastifyPluginAsyncZod } from "fastify-type-provider-zod"; import { z } from "zod"; import { hasPermission } from "@/auth"; -import { OrganizationRoleModel } from "@/models"; +import { createPaginatedResult } from "@/database/utils/pagination"; +import { OrganizationRoleModel, ROLE_SORT_COLUMNS } from "@/models"; import { ApiError, constructResponseSchema, + createPaginatedResponseSchema, + createSortingQuerySchema, + PaginationQuerySchema, SelectOrganizationRoleSchema, } from "@/types"; @@ -25,12 +29,22 @@ const organizationRoleRoutes: FastifyPluginAsyncZod = async (fastify) => { operationId: RouteId.GetRoles, description: "Get all roles in the organization", tags: ["Roles"], + querystring: z + .object({ + search: z.string().optional(), + }) + .merge(PaginationQuerySchema) + .merge(createSortingQuerySchema(ROLE_SORT_COLUMNS)), response: constructResponseSchema( - z.array(SelectOrganizationRoleSchema), + createPaginatedResponseSchema(SelectOrganizationRoleSchema), ), }, }, - async ({ organizationId, headers }, reply) => { + async ({ organizationId, headers, query }, reply) => { + const { search, limit, offset, sortBy, sortDirection } = query; + const pagination = { limit, offset }; + const sorting = { sortBy, sortDirection }; + const { success: canManageRoles } = await hasPermission( { ac: ["create"] }, headers, @@ -38,12 +52,37 @@ const organizationRoleRoutes: FastifyPluginAsyncZod = async (fastify) => { if (!canManageRoles) { // Non-admin users only see predefined roles + let predefinedRoles = + OrganizationRoleModel.getPredefinedOnly(organizationId); + + if (search) { + const searchLower = search.toLowerCase(); + predefinedRoles = predefinedRoles.filter( + (r) => + r.name.toLowerCase().includes(searchLower) || + (r.description?.toLowerCase().includes(searchLower) ?? false), + ); + } + + const total = predefinedRoles.length; + const paginatedRoles = predefinedRoles.slice( + pagination.offset, + pagination.offset + pagination.limit, + ); + return reply.send( - OrganizationRoleModel.getPredefinedOnly(organizationId), + createPaginatedResult(paginatedRoles, total, pagination), ); } - return reply.send(await OrganizationRoleModel.getAll(organizationId)); + return reply.send( + await OrganizationRoleModel.getAllPaginated({ + organizationId, + pagination, + sorting, + search, + }), + ); }, ); diff --git a/platform/backend/src/routes/organization.ts b/platform/backend/src/routes/organization.ts index dc488c40e7..53aefbdf0b 100644 --- a/platform/backend/src/routes/organization.ts +++ b/platform/backend/src/routes/organization.ts @@ -3,6 +3,7 @@ import { and, eq, inArray, like } from "drizzle-orm"; import type { FastifyPluginAsyncZod } from "fastify-type-provider-zod"; import { z } from "zod"; import db, { schema } from "@/database"; +import { MEMBER_SORT_COLUMNS } from "@/models/member"; import { ChatApiKeyModel, InteractionModel, @@ -17,6 +18,10 @@ import { ApiError, CompleteOnboardingSchema, constructResponseSchema, + createPaginatedResponseSchema, + createSortingQuerySchema, + MemberWithUserSchema, + PaginationQuerySchema, PublicAppearanceSchema, SelectOrganizationSchema, UpdateAppearanceSchema, @@ -412,6 +417,52 @@ const organizationRoutes: FastifyPluginAsyncZod = async (fastify) => { }, ); + fastify.get( + "/api/organization/members/paginated", + { + schema: { + operationId: RouteId.GetOrganizationMembersPaginated, + description: + "Get paginated members of the organization with search, sorting, and filtering", + tags: ["Organization"], + querystring: z + .object({ + search: z.string().optional(), + teamIds: z.string().optional(), + role: z.string().optional(), + }) + .merge(PaginationQuerySchema) + .merge(createSortingQuerySchema(MEMBER_SORT_COLUMNS)), + response: constructResponseSchema( + createPaginatedResponseSchema(MemberWithUserSchema), + ), + }, + }, + async ({ organizationId, query }, reply) => { + const { search, teamIds, role, limit, offset, sortBy, sortDirection } = + query; + + const parsedTeamIds = teamIds + ? teamIds + .split(",") + .map((id) => id.trim()) + .filter(Boolean) + : undefined; + + const result = + await MemberModel.findAllPaginatedByOrganization({ + organizationId, + pagination: { limit, offset }, + sorting: { sortBy, sortDirection }, + search, + teamIds: parsedTeamIds, + role, + }); + + return reply.send(result); + }, + ); + fastify.get( "/api/organization/appearance", { diff --git a/platform/backend/src/routes/team.ts b/platform/backend/src/routes/team.ts index 30c17b167a..e1e2ab1296 100644 --- a/platform/backend/src/routes/team.ts +++ b/platform/backend/src/routes/team.ts @@ -4,13 +4,17 @@ import { z } from "zod"; import { hasAnyAgentTypeAdminPermission, hasPermission } from "@/auth"; import config from "@/config"; import { AgentToolModel, TeamModel } from "@/models"; +import { TEAM_SORT_COLUMNS } from "@/models/team"; import { AddTeamExternalGroupBodySchema, AddTeamMemberBodySchema, ApiError, CreateTeamBodySchema, constructResponseSchema, + createPaginatedResponseSchema, + createSortingQuerySchema, DeleteObjectResponseSchema, + PaginationQuerySchema, SelectTeamExternalGroupSchema, SelectTeamMemberSchema, SelectTeamSchema, @@ -25,22 +29,33 @@ const teamRoutes: FastifyPluginAsyncZod = async (fastify) => { operationId: RouteId.GetTeams, description: "Get all teams in the organization", tags: ["Teams"], - response: constructResponseSchema(z.array(SelectTeamSchema)), + querystring: z + .object({ + search: z.string().optional(), + }) + .merge(PaginationQuerySchema) + .merge(createSortingQuerySchema(TEAM_SORT_COLUMNS)), + response: constructResponseSchema( + createPaginatedResponseSchema(SelectTeamSchema), + ), }, }, async (request, reply) => { + const { search, limit, offset, sortBy, sortDirection } = request.query; const { success: isTeamAdmin } = await hasPermission( { team: ["admin"] }, request.headers, ); - // Non-team admins only see teams they're members of - if (!isTeamAdmin) { - return reply.send(await TeamModel.getUserTeams(request.user.id)); - } - // Team admins see all teams in the organization return reply.send( - await TeamModel.findByOrganization(request.organizationId), + await TeamModel.findAllPaginated({ + organizationId: request.organizationId, + pagination: { limit, offset }, + sorting: { sortBy, sortDirection }, + search, + userId: request.user.id, + isTeamAdmin, + }), ); }, ); diff --git a/platform/backend/src/types/member.ts b/platform/backend/src/types/member.ts index 8dc095c5b6..13cf0b30b9 100644 --- a/platform/backend/src/types/member.ts +++ b/platform/backend/src/types/member.ts @@ -3,7 +3,7 @@ import { createSelectSchema, createUpdateSchema, } from "drizzle-zod"; -import type { z } from "zod"; +import { z } from "zod"; import { schema } from "@/database"; export const MemberSchema = createSelectSchema(schema.membersTable); @@ -13,3 +13,20 @@ const InsertMemberSchema = createInsertSchema(schema.membersTable); export type Member = z.infer; export type UpdateMember = z.infer; export type InsertMember = z.infer; + +export const MemberWithUserSchema = z.object({ + id: z.string(), + userId: z.string(), + name: z.string().nullable(), + email: z.string(), + role: z.string(), + createdAt: z.coerce.date(), + teams: z.array( + z.object({ + id: z.string(), + name: z.string(), + }), + ), + isPendingSignup: z.boolean(), +}); +export type MemberWithUser = z.infer; diff --git a/platform/e2e-tests/auth.teams.setup.ts b/platform/e2e-tests/auth.teams.setup.ts index a60ef3b7c6..92c9b668a5 100644 --- a/platform/e2e-tests/auth.teams.setup.ts +++ b/platform/e2e-tests/auth.teams.setup.ts @@ -109,7 +109,8 @@ async function getTeams(request: APIRequestContext): Promise { return []; } - return response.json(); + const body = await response.json(); + return body.data ?? body; } /** diff --git a/platform/e2e-tests/tests/api/access-control.ee.spec.ts b/platform/e2e-tests/tests/api/access-control.ee.spec.ts index 618415de0f..c39b6552d0 100644 --- a/platform/e2e-tests/tests/api/access-control.ee.spec.ts +++ b/platform/e2e-tests/tests/api/access-control.ee.spec.ts @@ -304,7 +304,8 @@ test.describe("Organization Roles API - Role Lifecycle", () => { method: "get", urlSuffix: "/api/roles", }); - const roles = await listResponse.json(); + const rolesBody = await listResponse.json(); + const roles = rolesBody.data; const foundRole = roles.find( (r: { id: string }) => r.id === createdRole.id, ); diff --git a/platform/e2e-tests/tests/api/access-control.spec.ts b/platform/e2e-tests/tests/api/access-control.spec.ts index 263cff9259..993c268b14 100644 --- a/platform/e2e-tests/tests/api/access-control.spec.ts +++ b/platform/e2e-tests/tests/api/access-control.spec.ts @@ -11,7 +11,9 @@ test.describe("Organization Roles API - Read Operations", () => { urlSuffix: "/api/roles", }); - const roles = await response.json(); + const rolesBody = await response.json(); + expect(rolesBody.data).toBeDefined(); + const roles = rolesBody.data; expect(Array.isArray(roles)).toBe(true); expect(roles.length).toBeGreaterThanOrEqual(2); // At least admin and member diff --git a/platform/e2e-tests/tests/api/chat-settings.spec.ts b/platform/e2e-tests/tests/api/chat-settings.spec.ts index b85949f40f..fede84fe03 100644 --- a/platform/e2e-tests/tests/api/chat-settings.spec.ts +++ b/platform/e2e-tests/tests/api/chat-settings.spec.ts @@ -481,7 +481,8 @@ test.describe("LLM Provider API Keys Team Scope", () => { method: "get", urlSuffix: "/api/teams", }); - const teams = await teamsResponse.json(); + const teamsBody = await teamsResponse.json(); + const teams = teamsBody.data ?? teamsBody; // Skip if no teams exist if (teams.length === 0) { diff --git a/platform/e2e-tests/tests/api/fixtures.ts b/platform/e2e-tests/tests/api/fixtures.ts index 37f905038d..4545b6b0a3 100644 --- a/platform/e2e-tests/tests/api/fixtures.ts +++ b/platform/e2e-tests/tests/api/fixtures.ts @@ -594,7 +594,8 @@ export const getTeamByName = async ( method: "get", urlSuffix: "/api/teams", }); - const teams = await teamsResponse.json(); + const teamsBody = await teamsResponse.json(); + const teams = teamsBody.data ?? teamsBody; const team = teams.find((t: { name: string }) => t.name === teamName); if (!team) { throw new Error(`Team '${teamName}' not found`); diff --git a/platform/e2e-tests/tests/api/teams-search.spec.ts b/platform/e2e-tests/tests/api/teams-search.spec.ts new file mode 100644 index 0000000000..ebc8eb810d --- /dev/null +++ b/platform/e2e-tests/tests/api/teams-search.spec.ts @@ -0,0 +1,305 @@ +import { expect, test } from "./fixtures"; + +test.describe("Teams Search and Filtering", () => { + test("should search teams by name", async ({ + request, + makeApiRequest, + createTeam, + deleteTeam, + }) => { + const prefix = `SearchName-${Date.now()}`; + const team1Response = await createTeam( + request, + `${prefix} Alpha Team`, + "Description one", + ); + const team1 = await team1Response.json(); + const team2Response = await createTeam( + request, + `${prefix} Beta Team`, + "Description two", + ); + const team2 = await team2Response.json(); + + try { + // Search for "Alpha" within our prefix + const searchResponse = await makeApiRequest({ + request, + method: "get", + urlSuffix: `/api/teams?search=${encodeURIComponent(`${prefix} Alpha`)}`, + }); + expect(searchResponse.status()).toBe(200); + const body = await searchResponse.json(); + expect(body.data).toBeDefined(); + expect(body.data.length).toBe(1); + expect(body.data[0].name).toContain("Alpha"); + expect(body.pagination).toBeDefined(); + expect(body.pagination.total).toBe(1); + } finally { + await deleteTeam(request, team1.id); + await deleteTeam(request, team2.id); + } + }); + + test("should search teams by description", async ({ + request, + makeApiRequest, + createTeam, + deleteTeam, + }) => { + const uniqueToken = `desctest-${Date.now()}`; + const teamResponse = await createTeam( + request, + `DescSearch Team ${uniqueToken}`, + `A unique ${uniqueToken} description here`, + ); + const team = await teamResponse.json(); + + try { + const searchResponse = await makeApiRequest({ + request, + method: "get", + urlSuffix: `/api/teams?search=${encodeURIComponent(uniqueToken)}`, + }); + expect(searchResponse.status()).toBe(200); + const body = await searchResponse.json(); + expect(body.data.length).toBeGreaterThanOrEqual(1); + expect( + body.data.some((t: { id: string }) => t.id === team.id), + ).toBe(true); + } finally { + await deleteTeam(request, team.id); + } + }); + + test("should search teams case-insensitively", async ({ + request, + makeApiRequest, + createTeam, + deleteTeam, + }) => { + const uniqueToken = `CaseTest-${Date.now()}`; + const teamResponse = await createTeam( + request, + `${uniqueToken} ENGINEERING`, + ); + const team = await teamResponse.json(); + + try { + // Search with lowercase + const response = await makeApiRequest({ + request, + method: "get", + urlSuffix: `/api/teams?search=${encodeURIComponent(`${uniqueToken} engineering`)}`, + }); + expect(response.status()).toBe(200); + const body = await response.json(); + expect( + body.data.some( + (t: { name: string }) => t.name === `${uniqueToken} ENGINEERING`, + ), + ).toBe(true); + } finally { + await deleteTeam(request, team.id); + } + }); + + test("should paginate teams", async ({ + request, + makeApiRequest, + createTeam, + deleteTeam, + }) => { + const prefix = `PaginateTest-${Date.now()}`; + const teams = []; + for (let i = 0; i < 3; i++) { + const r = await createTeam(request, `${prefix} Team ${i}`); + teams.push(await r.json()); + } + + try { + // First page with limit=2 + const page1 = await makeApiRequest({ + request, + method: "get", + urlSuffix: `/api/teams?search=${encodeURIComponent(prefix)}&limit=2&offset=0`, + }); + expect(page1.status()).toBe(200); + const body1 = await page1.json(); + expect(body1.data).toHaveLength(2); + expect(body1.pagination.total).toBe(3); + expect(body1.pagination.hasNext).toBe(true); + expect(body1.pagination.totalPages).toBe(2); + expect(body1.pagination.currentPage).toBe(1); + + // Second page + const page2 = await makeApiRequest({ + request, + method: "get", + urlSuffix: `/api/teams?search=${encodeURIComponent(prefix)}&limit=2&offset=2`, + }); + expect(page2.status()).toBe(200); + const body2 = await page2.json(); + expect(body2.data).toHaveLength(1); + expect(body2.pagination.hasNext).toBe(false); + expect(body2.pagination.currentPage).toBe(2); + } finally { + for (const team of teams) { + await deleteTeam(request, team.id); + } + } + }); + + test("should sort teams by name ascending", async ({ + request, + makeApiRequest, + createTeam, + deleteTeam, + }) => { + const prefix = `SortName-${Date.now()}`; + const teamA = await ( + await createTeam(request, `${prefix} Zebra`) + ).json(); + const teamB = await ( + await createTeam(request, `${prefix} Alpha`) + ).json(); + + try { + const response = await makeApiRequest({ + request, + method: "get", + urlSuffix: `/api/teams?search=${encodeURIComponent(prefix)}&sortBy=name&sortDirection=asc`, + }); + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body.data.length).toBe(2); + expect(body.data[0].name).toContain("Alpha"); + expect(body.data[1].name).toContain("Zebra"); + } finally { + await deleteTeam(request, teamA.id); + await deleteTeam(request, teamB.id); + } + }); + + test("should sort teams by createdAt", async ({ + request, + makeApiRequest, + createTeam, + deleteTeam, + }) => { + const prefix = `SortCreated-${Date.now()}`; + const teamFirst = await ( + await createTeam(request, `${prefix} First`) + ).json(); + const teamSecond = await ( + await createTeam(request, `${prefix} Second`) + ).json(); + + try { + // Sort ascending (oldest first) + const responseAsc = await makeApiRequest({ + request, + method: "get", + urlSuffix: `/api/teams?search=${encodeURIComponent(prefix)}&sortBy=createdAt&sortDirection=asc`, + }); + expect(responseAsc.status()).toBe(200); + const bodyAsc = await responseAsc.json(); + expect(bodyAsc.data.length).toBe(2); + expect(bodyAsc.data[0].name).toContain("First"); + expect(bodyAsc.data[1].name).toContain("Second"); + + // Sort descending (newest first) + const responseDesc = await makeApiRequest({ + request, + method: "get", + urlSuffix: `/api/teams?search=${encodeURIComponent(prefix)}&sortBy=createdAt&sortDirection=desc`, + }); + expect(responseDesc.status()).toBe(200); + const bodyDesc = await responseDesc.json(); + expect(bodyDesc.data[0].name).toContain("Second"); + expect(bodyDesc.data[1].name).toContain("First"); + } finally { + await deleteTeam(request, teamFirst.id); + await deleteTeam(request, teamSecond.id); + } + }); + + test("should return empty results for non-matching search", async ({ + request, + makeApiRequest, + }) => { + const response = await makeApiRequest({ + request, + method: "get", + urlSuffix: "/api/teams?search=nonexistent_term_xyz_123_456", + }); + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body.data).toHaveLength(0); + expect(body.pagination.total).toBe(0); + expect(body.pagination.hasNext).toBe(false); + }); + + test("should combine search with pagination", async ({ + request, + makeApiRequest, + createTeam, + deleteTeam, + }) => { + const prefix = `CombinedTest-${Date.now()}`; + const teams = []; + for (let i = 0; i < 5; i++) { + const r = await createTeam(request, `${prefix} Team ${String(i).padStart(2, "0")}`); + teams.push(await r.json()); + } + + try { + // Page 1 of search results + const page1 = await makeApiRequest({ + request, + method: "get", + urlSuffix: `/api/teams?search=${encodeURIComponent(prefix)}&limit=2&offset=0&sortBy=name&sortDirection=asc`, + }); + expect(page1.status()).toBe(200); + const body1 = await page1.json(); + expect(body1.data).toHaveLength(2); + expect(body1.pagination.total).toBe(5); + expect(body1.pagination.hasNext).toBe(true); + expect(body1.pagination.totalPages).toBe(3); + + // Page 2 + const page2 = await makeApiRequest({ + request, + method: "get", + urlSuffix: `/api/teams?search=${encodeURIComponent(prefix)}&limit=2&offset=2&sortBy=name&sortDirection=asc`, + }); + expect(page2.status()).toBe(200); + const body2 = await page2.json(); + expect(body2.data).toHaveLength(2); + expect(body2.pagination.hasNext).toBe(true); + + // Page 3 (last) + const page3 = await makeApiRequest({ + request, + method: "get", + urlSuffix: `/api/teams?search=${encodeURIComponent(prefix)}&limit=2&offset=4&sortBy=name&sortDirection=asc`, + }); + expect(page3.status()).toBe(200); + const body3 = await page3.json(); + expect(body3.data).toHaveLength(1); + expect(body3.pagination.hasNext).toBe(false); + + // Verify no duplicate items across pages + const allNames = [ + ...body1.data.map((t: { name: string }) => t.name), + ...body2.data.map((t: { name: string }) => t.name), + ...body3.data.map((t: { name: string }) => t.name), + ]; + expect(new Set(allNames).size).toBe(5); + } finally { + for (const team of teams) { + await deleteTeam(request, team.id); + } + } + }); +}); diff --git a/platform/e2e-tests/tests/api/teams.spec.ts b/platform/e2e-tests/tests/api/teams.spec.ts index 810d67946e..94d563b049 100644 --- a/platform/e2e-tests/tests/api/teams.spec.ts +++ b/platform/e2e-tests/tests/api/teams.spec.ts @@ -15,11 +15,12 @@ test.describe("Teams API", () => { }); expect(response.status()).toBe(200); - const teams = await response.json(); - expect(Array.isArray(teams)).toBe(true); + const body = await response.json(); + expect(body.data).toBeDefined(); + expect(Array.isArray(body.data)).toBe(true); // Admin should see both Engineering and Marketing teams (created in auth setup) - const teamNames = teams.map((t: { name: string }) => t.name); + const teamNames = body.data.map((t: { name: string }) => t.name); expect(teamNames).toContain(ENGINEERING_TEAM_NAME); expect(teamNames).toContain(MARKETING_TEAM_NAME); }); @@ -36,11 +37,12 @@ test.describe("Teams API", () => { }); expect(response.status()).toBe(200); - const teams = await response.json(); - expect(Array.isArray(teams)).toBe(true); + const body = await response.json(); + expect(body.data).toBeDefined(); + expect(Array.isArray(body.data)).toBe(true); // Member is only in Marketing Team (per auth setup) - const teamNames = teams.map((t: { name: string }) => t.name); + const teamNames = body.data.map((t: { name: string }) => t.name); expect(teamNames).toContain(MARKETING_TEAM_NAME); // Member should NOT see Engineering Team (they're not a member) expect(teamNames).not.toContain(ENGINEERING_TEAM_NAME); @@ -58,11 +60,12 @@ test.describe("Teams API", () => { }); expect(response.status()).toBe(200); - const teams = await response.json(); - expect(Array.isArray(teams)).toBe(true); + const body = await response.json(); + expect(body.data).toBeDefined(); + expect(Array.isArray(body.data)).toBe(true); // Editor is in both Engineering and Marketing Teams (per auth setup) - const teamNames = teams.map((t: { name: string }) => t.name); + const teamNames = body.data.map((t: { name: string }) => t.name); expect(teamNames).toContain(ENGINEERING_TEAM_NAME); expect(teamNames).toContain(MARKETING_TEAM_NAME); }); @@ -146,9 +149,10 @@ test.describe("Teams API", () => { urlSuffix: "/api/teams", }); expect(listResponse.status()).toBe(200); - const teams = await listResponse.json(); - expect(Array.isArray(teams)).toBe(true); - expect(teams.some((t: { id: string }) => t.id === team.id)).toBe(true); + const listBody = await listResponse.json(); + expect(listBody.data).toBeDefined(); + expect(Array.isArray(listBody.data)).toBe(true); + expect(listBody.data.some((t: { id: string }) => t.id === team.id)).toBe(true); // Cleanup await deleteTeam(request, team.id); diff --git a/platform/e2e-tests/tests/api/users-search.spec.ts b/platform/e2e-tests/tests/api/users-search.spec.ts new file mode 100644 index 0000000000..7611057b0e --- /dev/null +++ b/platform/e2e-tests/tests/api/users-search.spec.ts @@ -0,0 +1,312 @@ +import { expect, test } from "./fixtures"; + +test.describe("Users/Members Search and Filtering", () => { + test("should list paginated organization members", async ({ + request, + makeApiRequest, + }) => { + const response = await makeApiRequest({ + request, + method: "get", + urlSuffix: "/api/organization/members/paginated", + }); + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body.data).toBeDefined(); + expect(Array.isArray(body.data)).toBe(true); + expect(body.pagination).toBeDefined(); + expect(body.pagination.total).toBeGreaterThan(0); + expect(body.pagination.currentPage).toBeGreaterThanOrEqual(1); + expect(typeof body.pagination.hasNext).toBe("boolean"); + expect(body.pagination.totalPages).toBeGreaterThanOrEqual(1); + }); + + test("should search members by name", async ({ + request, + makeApiRequest, + }) => { + // First get all members to find a name to search for + const allResponse = await makeApiRequest({ + request, + method: "get", + urlSuffix: "/api/organization/members/paginated", + }); + const allBody = await allResponse.json(); + const memberWithName = allBody.data.find( + (m: { name: string | null }) => m.name && m.name.length >= 3, + ); + + if (memberWithName) { + const searchTerm = memberWithName.name.substring(0, 3); + const searchResponse = await makeApiRequest({ + request, + method: "get", + urlSuffix: `/api/organization/members/paginated?search=${encodeURIComponent(searchTerm)}`, + }); + expect(searchResponse.status()).toBe(200); + const searchBody = await searchResponse.json(); + expect(searchBody.data.length).toBeGreaterThan(0); + // Every result should match the search term in name or email + for (const member of searchBody.data) { + const matchesName = member.name + ?.toLowerCase() + .includes(searchTerm.toLowerCase()); + const matchesEmail = member.email + ?.toLowerCase() + .includes(searchTerm.toLowerCase()); + expect(matchesName || matchesEmail).toBe(true); + } + } + }); + + test("should search members by email", async ({ + request, + makeApiRequest, + }) => { + // Search for "example.com" which should match seeded test users + const response = await makeApiRequest({ + request, + method: "get", + urlSuffix: `/api/organization/members/paginated?search=${encodeURIComponent("example.com")}`, + }); + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body.data.length).toBeGreaterThan(0); + for (const member of body.data) { + const matchesName = member.name + ?.toLowerCase() + .includes("example.com"); + const matchesEmail = member.email + ?.toLowerCase() + .includes("example.com"); + expect(matchesName || matchesEmail).toBe(true); + } + }); + + test("should search members case-insensitively", async ({ + request, + makeApiRequest, + }) => { + // Get a member to use for case-insensitive search + const allResponse = await makeApiRequest({ + request, + method: "get", + urlSuffix: "/api/organization/members/paginated", + }); + const allBody = await allResponse.json(); + const firstMember = allBody.data[0]; + + if (firstMember?.email) { + // Search with uppercase version of email + const uppercaseTerm = firstMember.email.toUpperCase(); + const response = await makeApiRequest({ + request, + method: "get", + urlSuffix: `/api/organization/members/paginated?search=${encodeURIComponent(uppercaseTerm)}`, + }); + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body.data.length).toBeGreaterThan(0); + expect( + body.data.some( + (m: { email: string }) => + m.email.toLowerCase() === firstMember.email.toLowerCase(), + ), + ).toBe(true); + } + }); + + test("should filter members by role", async ({ + request, + makeApiRequest, + }) => { + const response = await makeApiRequest({ + request, + method: "get", + urlSuffix: "/api/organization/members/paginated?role=admin", + }); + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body.data.length).toBeGreaterThan(0); + // All returned members should have admin role + for (const member of body.data) { + expect(member.role).toBe("admin"); + } + }); + + test("should filter members by team", async ({ + request, + makeApiRequest, + }) => { + // Get teams to find one to filter by + const allTeamsResponse = await makeApiRequest({ + request, + method: "get", + urlSuffix: "/api/teams", + }); + const teamsBody = await allTeamsResponse.json(); + const teams = teamsBody.data; + + if (teams && teams.length > 0) { + const teamId = teams[0].id; + const response = await makeApiRequest({ + request, + method: "get", + urlSuffix: `/api/organization/members/paginated?teamIds=${teamId}`, + }); + expect(response.status()).toBe(200); + const body = await response.json(); + // All returned members should belong to the specified team + for (const member of body.data) { + expect( + member.teams.some((t: { id: string }) => t.id === teamId), + ).toBe(true); + } + } + }); + + test("should support pagination with limit and offset", async ({ + request, + makeApiRequest, + }) => { + const response = await makeApiRequest({ + request, + method: "get", + urlSuffix: "/api/organization/members/paginated?limit=1&offset=0", + }); + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body.data.length).toBeLessThanOrEqual(1); + expect(body.pagination.limit).toBe(1); + expect(body.pagination.currentPage).toBe(1); + + // If there are more members, verify next page works + if (body.pagination.hasNext) { + const page2 = await makeApiRequest({ + request, + method: "get", + urlSuffix: "/api/organization/members/paginated?limit=1&offset=1", + }); + expect(page2.status()).toBe(200); + const body2 = await page2.json(); + expect(body2.data.length).toBeLessThanOrEqual(1); + expect(body2.pagination.currentPage).toBe(2); + + // Verify different members on each page + if (body.data.length > 0 && body2.data.length > 0) { + expect(body.data[0].email).not.toBe(body2.data[0].email); + } + } + }); + + test("should sort members by email ascending", async ({ + request, + makeApiRequest, + }) => { + const response = await makeApiRequest({ + request, + method: "get", + urlSuffix: + "/api/organization/members/paginated?sortBy=email&sortDirection=asc", + }); + expect(response.status()).toBe(200); + const body = await response.json(); + if (body.data.length > 1) { + for (let i = 1; i < body.data.length; i++) { + expect( + body.data[i].email.localeCompare(body.data[i - 1].email), + ).toBeGreaterThanOrEqual(0); + } + } + }); + + test("should sort members by name descending", async ({ + request, + makeApiRequest, + }) => { + const response = await makeApiRequest({ + request, + method: "get", + urlSuffix: + "/api/organization/members/paginated?sortBy=name&sortDirection=desc", + }); + expect(response.status()).toBe(200); + const body = await response.json(); + const membersWithNames = body.data.filter( + (m: { name: string | null }) => m.name != null, + ); + if (membersWithNames.length > 1) { + for (let i = 1; i < membersWithNames.length; i++) { + expect( + membersWithNames[i - 1].name.localeCompare( + membersWithNames[i].name, + ), + ).toBeGreaterThanOrEqual(0); + } + } + }); + + test("should return empty results for non-matching search", async ({ + request, + makeApiRequest, + }) => { + const response = await makeApiRequest({ + request, + method: "get", + urlSuffix: + "/api/organization/members/paginated?search=nonexistent_user_xyz_123_456", + }); + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body.data).toHaveLength(0); + expect(body.pagination.total).toBe(0); + }); + + test("should include teams array and isPendingSignup in member data", async ({ + request, + makeApiRequest, + }) => { + const response = await makeApiRequest({ + request, + method: "get", + urlSuffix: "/api/organization/members/paginated", + }); + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body.data.length).toBeGreaterThan(0); + for (const member of body.data) { + expect(Array.isArray(member.teams)).toBe(true); + expect(typeof member.isPendingSignup).toBe("boolean"); + // Each team in the array should have an id + for (const team of member.teams) { + expect(team.id).toBeDefined(); + } + } + }); + + test("should combine search with role filter", async ({ + request, + makeApiRequest, + }) => { + // Search for "example.com" and filter to admin role + const response = await makeApiRequest({ + request, + method: "get", + urlSuffix: + "/api/organization/members/paginated?search=example.com&role=admin", + }); + expect(response.status()).toBe(200); + const body = await response.json(); + // All results should be admins matching the search + for (const member of body.data) { + expect(member.role).toBe("admin"); + const matchesName = member.name + ?.toLowerCase() + .includes("example.com"); + const matchesEmail = member.email + ?.toLowerCase() + .includes("example.com"); + expect(matchesName || matchesEmail).toBe(true); + } + }); +}); diff --git a/platform/e2e-tests/tests/ui/identity-providers.ee.spec.ts b/platform/e2e-tests/tests/ui/identity-providers.ee.spec.ts index 8899dc55d3..95ae76458e 100644 --- a/platform/e2e-tests/tests/ui/identity-providers.ee.spec.ts +++ b/platform/e2e-tests/tests/ui/identity-providers.ee.spec.ts @@ -295,7 +295,8 @@ test.describe("Identity Provider Team Sync E2E", () => { const teamResponse = await page.request.get( `http://localhost:9000/api/teams`, ); - const teams = await teamResponse.json(); + const teamsBody = await teamResponse.json(); + const teams = teamsBody.data ?? teamsBody; const createdTeam = teams.find( (t: { name: string }) => t.name === teamName, ); diff --git a/platform/e2e-tests/utils.ts b/platform/e2e-tests/utils.ts index f72544fc8d..3affe52b62 100644 --- a/platform/e2e-tests/utils.ts +++ b/platform/e2e-tests/utils.ts @@ -427,26 +427,27 @@ export async function assignEngineeringTeamToDefaultProfileViaApi({ `Failed to get teams: ${JSON.stringify(teamsResponse.error)}`, ); } - if (!teamsResponse.data || teamsResponse.data.length === 0) { + const teams = teamsResponse.data?.data; + if (!teams || teams.length === 0) { throw new Error( `No teams returned from API. Response: ${JSON.stringify(teamsResponse)}`, ); } - const defaultTeam = teamsResponse.data.find( + const defaultTeam = teams.find( (team) => team.name === DEFAULT_TEAM_NAME, ); if (!defaultTeam) { - const teamNames = teamsResponse.data.map((t) => t.name).join(", "); + const teamNames = teams.map((t) => t.name).join(", "); throw new Error( `Team "${DEFAULT_TEAM_NAME}" not found. Available teams: [${teamNames}]`, ); } - const engineeringTeam = teamsResponse.data.find( + const engineeringTeam = teams.find( (team) => team.name === ENGINEERING_TEAM_NAME, ); if (!engineeringTeam) { - const teamNames = teamsResponse.data.map((t) => t.name).join(", "); + const teamNames = teams.map((t) => t.name).join(", "); throw new Error( `Team "${ENGINEERING_TEAM_NAME}" not found. Available teams: [${teamNames}]`, ); @@ -509,9 +510,9 @@ export async function createTeamMcpGatewayViaApi({ `Failed to get teams: ${JSON.stringify(teamsResponse.error)}`, ); } - const team = teamsResponse.data?.find((t) => t.name === teamName); + const team = teamsResponse.data?.data?.find((t) => t.name === teamName); if (!team) { - const teamNames = teamsResponse.data?.map((t) => t.name).join(", "); + const teamNames = teamsResponse.data?.data?.map((t) => t.name).join(", "); throw new Error( `Team "${teamName}" not found. Available teams: [${teamNames}]`, ); diff --git a/platform/frontend/src/app/agents/page.client.tsx b/platform/frontend/src/app/agents/page.client.tsx index e806c5aa17..a3f5c6803b 100644 --- a/platform/frontend/src/app/agents/page.client.tsx +++ b/platform/frontend/src/app/agents/page.client.tsx @@ -64,7 +64,7 @@ import { AgentActions } from "./agent-actions"; type AgentsInitialData = { agents: archestraApiTypes.GetAgentsResponses["200"] | null; - teams: archestraApiTypes.GetTeamsResponses["200"]; + teams: archestraApiTypes.GetTeamsResponses["200"]["data"]; }; export default function AgentsPage({ @@ -237,7 +237,7 @@ function Agents({ initialData }: { initialData?: AgentsInitialData }) { queryKey: ["teams"], queryFn: async () => { const { data } = await archestraApiSdk.getTeams(); - return data || []; + return data?.data || []; }, initialData: initialData?.teams, }); @@ -360,8 +360,15 @@ function Agents({ initialData }: { initialData?: AgentsInitialData }) { const scope = agent.scope; return (
-
- {agent.name} +
+ + + + {agent.name} + + {agent.name} + + {agent.labels && agent.labels.length > 0 && ( diff --git a/platform/frontend/src/app/agents/page.tsx b/platform/frontend/src/app/agents/page.tsx index b4ffbf7202..2b230db52a 100644 --- a/platform/frontend/src/app/agents/page.tsx +++ b/platform/frontend/src/app/agents/page.tsx @@ -19,7 +19,7 @@ export const dynamic = "force-dynamic"; export default async function AgentsPageServer() { let initialData: { agents: archestraApiTypes.GetAgentsResponses["200"] | null; - teams: archestraApiTypes.GetTeamsResponses["200"]; + teams: archestraApiTypes.GetTeamsResponses["200"]["data"]; } = { agents: null, teams: [], @@ -47,7 +47,7 @@ export default async function AgentsPageServer() { } initialData = { agents: agentsResponse.data || null, - teams: teamsResponse.data || [], + teams: teamsResponse.data?.data || [], }; } catch (error) { return ; diff --git a/platform/frontend/src/app/knowledge/knowledge-bases/_parts/create-connector-dialog.tsx b/platform/frontend/src/app/knowledge/knowledge-bases/_parts/create-connector-dialog.tsx index 9470b8e586..088caf1cf3 100644 --- a/platform/frontend/src/app/knowledge/knowledge-bases/_parts/create-connector-dialog.tsx +++ b/platform/frontend/src/app/knowledge/knowledge-bases/_parts/create-connector-dialog.tsx @@ -124,7 +124,7 @@ export function CreateConnectorDialog({ connectorType: values.connectorType, config: config as archestraApiTypes.CreateConnectorData["body"]["config"], credentials: { - email: values.email, + ...(values.email && { email: values.email }), apiToken: values.apiToken, }, schedule: values.schedule, @@ -148,7 +148,9 @@ export function CreateConnectorDialog({ }; const urlConfig = getUrlConfig(connectorType); + const isCloud = form.watch("config.isCloud") as boolean | undefined; const needsEmail = connectorType === "jira" || connectorType === "confluence"; + const emailRequired = needsEmail && isCloud !== false; return ( @@ -277,17 +279,33 @@ export function CreateConnectorDialog({ ( - Email + + Email{!emailRequired && " (optional)"} + + {!emailRequired && ( + + Leave empty to authenticate with a personal access + token instead. + + )} )} @@ -299,20 +317,28 @@ export function CreateConnectorDialog({ name="apiToken" rules={{ required: needsEmail - ? "API token is required" + ? emailRequired + ? "API token is required" + : "API token or personal access token is required" : "Personal access token is required", }} render={({ field }) => ( - {needsEmail ? "API Token" : "Personal Access Token"} + {needsEmail + ? emailRequired + ? "API Token" + : "API Token / Personal Access Token" + : "Personal Access Token"} { const { data } = await archestraApiSdk.getTeams(); - return data || []; + return data?.data || []; }, initialData: initialData?.teams, }); @@ -334,7 +334,14 @@ function LlmProxies({ initialData }: { initialData?: LlmProxiesInitialData }) { return (
- {agent.name} + + + + {agent.name} + + {agent.name} + + {agent.agentType === "profile" && ( diff --git a/platform/frontend/src/app/llm/proxies/page.tsx b/platform/frontend/src/app/llm/proxies/page.tsx index cf437ac6e6..00512153b5 100644 --- a/platform/frontend/src/app/llm/proxies/page.tsx +++ b/platform/frontend/src/app/llm/proxies/page.tsx @@ -19,7 +19,7 @@ export const dynamic = "force-dynamic"; export default async function LlmProxiesPageServer() { let initialData: { agents: archestraApiTypes.GetAgentsResponses["200"] | null; - teams: archestraApiTypes.GetTeamsResponses["200"]; + teams: archestraApiTypes.GetTeamsResponses["200"]["data"]; } = { agents: null, teams: [], @@ -47,7 +47,7 @@ export default async function LlmProxiesPageServer() { } initialData = { agents: agentsResponse.data || null, - teams: teamsResponse.data || [], + teams: teamsResponse.data?.data || [], }; } catch (error) { return ; diff --git a/platform/frontend/src/app/mcp/gateways/page.client.tsx b/platform/frontend/src/app/mcp/gateways/page.client.tsx index a7ea90ddb9..e812a2037d 100644 --- a/platform/frontend/src/app/mcp/gateways/page.client.tsx +++ b/platform/frontend/src/app/mcp/gateways/page.client.tsx @@ -70,7 +70,7 @@ import { McpGatewayActions } from "./mcp-gateway-actions"; type McpGatewaysInitialData = { agents: archestraApiTypes.GetAgentsResponses["200"] | null; - teams: archestraApiTypes.GetTeamsResponses["200"]; + teams: archestraApiTypes.GetTeamsResponses["200"]["data"]; }; export default function McpGatewaysPage({ @@ -249,7 +249,7 @@ function McpGateways({ queryKey: ["teams"], queryFn: async () => { const { data } = await archestraApiSdk.getTeams(); - return data || []; + return data?.data || []; }, initialData: initialData?.teams, }); @@ -346,7 +346,14 @@ function McpGateways({ return (
- {agent.name} + + + + {agent.name} + + {agent.name} + + {agent.agentType === "profile" && ( diff --git a/platform/frontend/src/app/mcp/gateways/page.tsx b/platform/frontend/src/app/mcp/gateways/page.tsx index 9d6ab34de9..da4686c341 100644 --- a/platform/frontend/src/app/mcp/gateways/page.tsx +++ b/platform/frontend/src/app/mcp/gateways/page.tsx @@ -19,7 +19,7 @@ export const dynamic = "force-dynamic"; export default async function McpGatewaysPageServer() { let initialData: { agents: archestraApiTypes.GetAgentsResponses["200"] | null; - teams: archestraApiTypes.GetTeamsResponses["200"]; + teams: archestraApiTypes.GetTeamsResponses["200"]["data"]; } = { agents: null, teams: [], @@ -47,7 +47,7 @@ export default async function McpGatewaysPageServer() { } initialData = { agents: agentsResponse.data || null, - teams: teamsResponse.data || [], + teams: teamsResponse.data?.data || [], }; } catch (error) { return ; diff --git a/platform/frontend/src/app/settings/users/page.tsx b/platform/frontend/src/app/settings/users/page.tsx index 0821c30bbe..d42e900ff5 100644 --- a/platform/frontend/src/app/settings/users/page.tsx +++ b/platform/frontend/src/app/settings/users/page.tsx @@ -1,13 +1,26 @@ "use client"; -import { OrganizationMembersCard } from "@daveyplate/better-auth-ui"; import { useQueryClient } from "@tanstack/react-query"; -import { useState } from "react"; +import type { ColumnDef, SortingState } from "@tanstack/react-table"; +import { + ChevronDown, + ChevronUp, + Copy, + Plus, + Trash2, + Users, + X, +} from "lucide-react"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { toast } from "sonner"; import { ErrorBoundary } from "@/app/_parts/error-boundary"; import { InvitationsList } from "@/components/invitations-list"; import { InviteByLinkCard } from "@/components/invite-by-link-card"; import { LoadingSpinner, LoadingWrapper } from "@/components/loading"; -import { PendingSignupMembers } from "@/components/pending-signup-members"; +import { SearchInput } from "@/components/search-input"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { Card, CardContent, @@ -15,41 +28,494 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; +import { DataTable } from "@/components/ui/data-table"; import { Dialog, DialogContent, + DialogDescription, + DialogFooter, + DialogForm, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; +import { MultiSelect } from "@/components/ui/multi-select"; +import { PermissionButton } from "@/components/ui/permission-button"; +import { RoleSelect } from "@/components/ui/role-select"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { TooltipButton } from "@/components/ui/tooltip-button"; import { useHasPermissions } from "@/lib/auth.query"; - -import { useChatOpsStatus } from "@/lib/chatops.query"; import config from "@/lib/config"; import { organizationKeys, - useActiveMemberRole, + type PaginatedMember, useActiveOrganization, + useDeletePendingSignupMember, useMemberSignupStatus, + useOrganizationMembersPaginated, + useRemoveMember, + useUpdateMemberRole, } from "@/lib/organization.query"; +import { useRoles } from "@/lib/role.query"; +import { useTeams } from "@/lib/team.query"; +import { + DEFAULT_SORT_BY, + DEFAULT_SORT_DIRECTION, + DEFAULT_TABLE_LIMIT, + formatDate, +} from "@/lib/utils"; + +function SortIcon({ isSorted }: { isSorted: false | "asc" | "desc" }) { + const upArrow = ; + const downArrow = ; + if (isSorted === "asc") { + return upArrow; + } + if (isSorted === "desc") { + return downArrow; + } + return ( +
+ {upArrow} + {downArrow} +
+ ); +} + +function TeamsBadges({ + teams, +}: { + teams: Array<{ id: string; name: string }>; +}) { + const MAX_TEAMS_TO_SHOW = 3; + + if (!teams || teams.length === 0) { + return -; + } + + const visibleTeams = teams.slice(0, MAX_TEAMS_TO_SHOW); + const remainingTeams = teams.slice(MAX_TEAMS_TO_SHOW); + + return ( +
+ {visibleTeams.map((team) => ( + + + {team.name} + + ))} + {remainingTeams.length > 0 && ( + + + + + +{remainingTeams.length} more + + + +
+ {remainingTeams.map((team) => ( +
+ {team.name} +
+ ))} +
+
+
+
+ )} +
+ ); +} function MembersSettingsContent() { const queryClient = useQueryClient(); - const { data: activeOrg, isPending } = useActiveOrganization(); - const { data: activeMemberRole } = useActiveMemberRole(activeOrg?.id); - const { data: chatOpsProviders } = useChatOpsStatus(); - const hasChatOps = chatOpsProviders?.some((p) => p.configured); + const searchParams = useSearchParams(); + const router = useRouter(); + const pathname = usePathname(); + + const { data: activeOrg, isPending: isOrgPending } = useActiveOrganization(); + const { data: canInvite } = useHasPermissions({ invitation: ["create"] }); + const { data: canManageMembers } = useHasPermissions({ + member: ["update"], + }); + const invitationsEnabled = !config.disableInvitations; + + // URL params + const pageFromUrl = searchParams.get("page"); + const pageSizeFromUrl = searchParams.get("pageSize"); + const searchFilter = searchParams.get("search") || ""; + const sortByFromUrl = searchParams.get("sortBy") as + | "name" + | "email" + | "role" + | "createdAt" + | null; + const sortDirectionFromUrl = searchParams.get("sortDirection") as + | "asc" + | "desc" + | null; + const teamIdsFromUrl = searchParams.get("teamIds"); + const roleFromUrl = searchParams.get("role"); + + const pageSize = Number(pageSizeFromUrl || DEFAULT_TABLE_LIMIT); + const pageIndex = Number(pageFromUrl || "1") - 1; + const offset = pageIndex * pageSize; + + const sortBy = sortByFromUrl || DEFAULT_SORT_BY; + const sortDirection = sortDirectionFromUrl || DEFAULT_SORT_DIRECTION; + + const { data: membersResponse, isPending: isMembersPending } = + useOrganizationMembersPaginated({ + limit: pageSize, + offset, + sortBy, + sortDirection, + search: searchFilter || undefined, + teamIds: teamIdsFromUrl || undefined, + role: roleFromUrl || undefined, + }); + + const { data: teams } = useTeams(); + const { data: roles } = useRoles(); const { data: signupStatus } = useMemberSignupStatus(); + + // Build invitation ID lookup for pending signup members + const invitationByUserId = useMemo(() => { + const map = new Map(); + for (const m of signupStatus?.pendingSignupMembers ?? []) { + if (m.invitationId) { + map.set(m.userId, m.invitationId); + } + } + return map; + }, [signupStatus]); + + const [sorting, setSorting] = useState([ + { id: sortBy, desc: sortDirection === "desc" }, + ]); + + useEffect(() => { + setSorting([{ id: sortBy, desc: sortDirection === "desc" }]); + }, [sortBy, sortDirection]); + + // Dialog state const [inviteDialogOpen, setInviteDialogOpen] = useState(false); const [refreshKey, setRefreshKey] = useState(0); + const [roleUpdateMember, setRoleUpdateMember] = + useState(null); + const [removingMember, setRemovingMember] = useState( + null, + ); - const { data: canInvite } = useHasPermissions({ invitation: ["create"] }); - const invitationsEnabled = !config.disableInvitations; - const pendingSignupMembers = signupStatus?.pendingSignupMembers ?? []; - const pendingUserIds = new Set(pendingSignupMembers.map((m) => m.userId)); + // Filter state + const selectedTeamIds = useMemo( + () => (teamIdsFromUrl ? teamIdsFromUrl.split(",") : []), + [teamIdsFromUrl], + ); + + const teamItems = useMemo( + () => (teams ?? []).map((t) => ({ value: t.id, label: t.name })), + [teams], + ); + + const hasActiveFilters = !!(searchFilter || teamIdsFromUrl || roleFromUrl); + + // URL update helpers + const updateUrlParams = useCallback( + (updates: Record) => { + const params = new URLSearchParams(searchParams.toString()); + for (const [key, value] of Object.entries(updates)) { + if (value === null || value === "") { + params.delete(key); + } else { + params.set(key, value); + } + } + params.set("page", "1"); + router.push(`${pathname}?${params.toString()}`, { scroll: false }); + }, + [searchParams, router, pathname], + ); + + const handleSortingChange = useCallback( + (updater: SortingState | ((old: SortingState) => SortingState)) => { + const newSorting = + typeof updater === "function" ? updater(sorting) : updater; + setSorting(newSorting); + + const params = new URLSearchParams(searchParams.toString()); + if (newSorting.length > 0) { + params.set("sortBy", newSorting[0].id); + params.set("sortDirection", newSorting[0].desc ? "desc" : "asc"); + } else { + params.delete("sortBy"); + params.delete("sortDirection"); + } + params.set("page", "1"); + router.push(`${pathname}?${params.toString()}`, { scroll: false }); + }, + [sorting, searchParams, router, pathname], + ); + + const handlePaginationChange = useCallback( + (newPagination: { pageIndex: number; pageSize: number }) => { + const params = new URLSearchParams(searchParams.toString()); + params.set("page", String(newPagination.pageIndex + 1)); + params.set("pageSize", String(newPagination.pageSize)); + router.push(`${pathname}?${params.toString()}`, { scroll: false }); + }, + [searchParams, router, pathname], + ); - const members = activeOrg ? ( + const handleTeamIdsChange = useCallback( + (values: string[]) => { + updateUrlParams({ + teamIds: values.length > 0 ? values.join(",") : null, + }); + }, + [updateUrlParams], + ); + + const handleRoleChange = useCallback( + (value: string) => { + updateUrlParams({ + role: value === "all" ? null : value, + }); + }, + [updateUrlParams], + ); + + const handleClearFilters = useCallback(() => { + updateUrlParams({ + search: null, + teamIds: null, + role: null, + }); + }, [updateUrlParams]); + + const handleCopyInvitationLink = useCallback( + async (member: PaginatedMember) => { + const invitationId = invitationByUserId.get(member.userId); + if (!invitationId) { + toast.error("No invitation link available for this user"); + return; + } + const link = `${window.location.origin}/auth/sign-up-with-invitation?invitationId=${invitationId}&email=${encodeURIComponent(member.email)}`; + await navigator.clipboard.writeText(link); + toast.success("Invitation link copied to clipboard"); + }, + [invitationByUserId], + ); + + const members = membersResponse?.data || []; + const pagination = membersResponse?.pagination; + + const columns: ColumnDef[] = [ + { + id: "name", + accessorKey: "name", + size: 200, + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const member = row.original; + return ( +
+
+ + {member.name || "Unknown"} + + {member.isPendingSignup && ( + + Pending signup + + )} +
+
+ ); + }, + }, + { + id: "email", + accessorKey: "email", + size: 250, + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {row.original.email} +
+ ), + }, + { + id: "role", + accessorKey: "role", + size: 120, + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + {toTitleCase(row.original.role)} + + ), + }, + { + id: "teams", + header: "Teams", + enableSorting: false, + cell: ({ row }) => , + }, + { + id: "createdAt", + accessorKey: "createdAt", + size: 160, + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {formatDate({ date: row.original.createdAt })} +
+ ), + }, + ...(canManageMembers + ? [ + { + id: "actions", + header: "Actions", + size: 160, + enableHiding: false, + enableSorting: false, + cell: ({ row }: { row: { original: PaginatedMember } }) => { + const member = row.original; + + if (member.isPendingSignup) { + const hasInvitation = invitationByUserId.has(member.userId); + return ( +
+ {hasInvitation && ( + handleCopyInvitationLink(member)} + > + + + )} + setRemovingMember(member)} + > + + +
+ ); + } + + return ( +
+ setRoleUpdateMember(member)} + > + Update Role + + setRemovingMember(member)} + > + + +
+ ); + }, + } satisfies ColumnDef, + ] + : []), + ]; + + if (isOrgPending) { + return ; + } + + if (!activeOrg) { + return ( + + + No Organization + + You are not part of any organization yet. + + + +

+ An organization will be created for you automatically. Please + refresh the page or sign out and sign in again. +

+
+
+ ); + } + + return (
- {invitationsEnabled && activeMemberRole && canInvite && ( + {/* Invite dialog */} + {invitationsEnabled && canInvite && ( { @@ -58,6 +524,9 @@ function MembersSettingsContent() { queryClient.invalidateQueries({ queryKey: organizationKeys.invitations(), }); + queryClient.invalidateQueries({ + queryKey: organizationKeys.all, + }); } }} > @@ -72,54 +541,253 @@ function MembersSettingsContent() { )} - !pendingUserIds.has(member.userId)} - action={ - invitationsEnabled - ? () => { - setInviteDialogOpen(true); - } - : undefined - } - /> - {hasChatOps && ( - + + {/* Header with invite button */} +
+
+

Users

+

+ Manage users and their roles in your organization. +

+
+ {invitationsEnabled && canInvite && ( + + )} +
+ + {/* Search and filters */} +
+
+ + + `${n} ${n === 1 ? "team" : "teams"} selected` + } + /> + + {hasActiveFilters && ( + + )} +
+
+ + {/* Members DataTable */} + } + > + {members.length === 0 ? ( +
+ {searchFilter || teamIdsFromUrl || roleFromUrl + ? "No users found matching your filters" + : "No users found"} +
+ ) : ( + + )} +
+ + {/* Update Role Dialog */} + {roleUpdateMember && activeOrg && ( + !open && setRoleUpdateMember(null)} + /> + )} + + {/* Remove Member Dialog */} + {removingMember && activeOrg && ( + !open && setRemovingMember(null)} + /> )} + + {/* Pending Invitations */} {invitationsEnabled && ( )}
- ) : ( - - - No Organization - - You are not part of any organization yet. - - - -

- An organization will be created for you automatically. Please refresh - the page or sign out and sign in again. -

-
-
); +} + +function UpdateRoleDialog({ + member, + organizationId, + open, + onOpenChange, +}: { + member: PaginatedMember; + organizationId: string; + open: boolean; + onOpenChange: (open: boolean) => void; +}) { + const [selectedRole, setSelectedRole] = useState(member.role); + const updateMemberRole = useUpdateMemberRole(); + + const handleSubmit = useCallback(async () => { + if (selectedRole === member.role) { + toast.info("Role is the same, no changes made"); + onOpenChange(false); + return; + } + + const result = await updateMemberRole.mutateAsync({ + memberId: member.id, + role: selectedRole, + organizationId, + }); + + if (result) { + onOpenChange(false); + } + }, [selectedRole, member, organizationId, updateMemberRole, onOpenChange]); return ( - }> - {members} - + + + + Update Role + + Change the role for {member.name || member.email}. + + + +
+ +
+ + + + +
+
+
+ ); +} + +function RemoveMemberDialog({ + member, + organizationId, + open, + onOpenChange, +}: { + member: PaginatedMember; + organizationId: string; + open: boolean; + onOpenChange: (open: boolean) => void; +}) { + const removeMember = useRemoveMember(); + const deletePendingMember = useDeletePendingSignupMember(); + + const isPending = removeMember.isPending || deletePendingMember.isPending; + + const handleSubmit = useCallback(async () => { + if (member.isPendingSignup) { + const result = await deletePendingMember.mutateAsync(member.userId); + if (result) { + onOpenChange(false); + } + } else { + const result = await removeMember.mutateAsync({ + memberId: member.id, + organizationId, + }); + if (result) { + onOpenChange(false); + } + } + }, [member, organizationId, removeMember, deletePendingMember, onOpenChange]); + + return ( + + + + Remove User + + Are you sure you want to remove{" "} + {member.name || member.email}{" "} + from the organization? This action cannot be undone. + + + + + + + + + + ); } @@ -130,3 +798,10 @@ export default function MembersSettingsPage() { ); } + +function toTitleCase(str: string): string { + return str + .split(/[-_\s]+/) + .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(" "); +} diff --git a/platform/frontend/src/components/agent-dialog.tsx b/platform/frontend/src/components/agent-dialog.tsx index 02a00099b1..8b252f44bd 100644 --- a/platform/frontend/src/components/agent-dialog.tsx +++ b/platform/frontend/src/components/agent-dialog.tsx @@ -596,7 +596,7 @@ export function AgentDialog({ queryKey: ["teams"], queryFn: async () => { const response = await archestraApiSdk.getTeams(); - return response.data || []; + return response.data?.data || []; }, }); const resource = getResourceForAgentType(agentType); diff --git a/platform/frontend/src/components/chat/initial-agent-selector.tsx b/platform/frontend/src/components/chat/initial-agent-selector.tsx index da3782dd33..7baac0091f 100644 --- a/platform/frontend/src/components/chat/initial-agent-selector.tsx +++ b/platform/frontend/src/components/chat/initial-agent-selector.tsx @@ -444,10 +444,10 @@ function AgentSettingsView({ const [isSaving, setIsSaving] = useState(false); const debounceRef = useRef | null>(null); - // biome-ignore lint/correctness/useExhaustiveDependencies: agent?.id ensures reset when switching agents + // biome-ignore lint/correctness/useExhaustiveDependencies: only reset when switching agents, not on systemPrompt changes from autosave useEffect(() => { setInstructions(agent?.systemPrompt ?? ""); - }, [agent?.id, agent?.systemPrompt]); + }, [agent?.id]); const saveInstructions = useCallback( (value: string) => { diff --git a/platform/frontend/src/components/roles/roles-list.ee.tsx b/platform/frontend/src/components/roles/roles-list.ee.tsx index 8487fcd9da..e483c09c71 100644 --- a/platform/frontend/src/components/roles/roles-list.ee.tsx +++ b/platform/frontend/src/components/roles/roles-list.ee.tsx @@ -5,20 +5,21 @@ import { DocsPage, getDocsUrl, type Permissions, + type PredefinedRoleName, + roleDescriptions, } from "@shared"; import { allAvailableActions } from "@shared/access-control"; -import { Plus, Shield, Trash2 } from "lucide-react"; -import { useCallback, useState } from "react"; +import type { ColumnDef, SortingState } from "@tanstack/react-table"; +import { ChevronDown, ChevronUp, Plus, Shield, Trash2 } from "lucide-react"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; -import { PredefinedRoles } from "@/components/roles/predefined-roles"; +import { LoadingSpinner, LoadingWrapper } from "@/components/loading"; +import { PageLayout } from "@/components/page-layout"; +import { SearchInput } from "@/components/search-input"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; +import { DataTable } from "@/components/ui/data-table"; import { Dialog, DialogContent, @@ -31,22 +32,54 @@ import { import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { PermissionButton } from "@/components/ui/permission-button"; +import { Textarea } from "@/components/ui/textarea"; import { useCreateRole, useDeleteRole, - useRoles, + useRolesPaginated, useUpdateRole, } from "@/lib/role.query"; +import { DEFAULT_TABLE_LIMIT, formatDate } from "@/lib/utils"; import { RolePermissionBuilder } from "./role-permission-builder.ee"; -type Role = archestraApiTypes.GetRoleResponses["200"]; +type RoleData = archestraApiTypes.GetRolesResponses["200"]["data"][number]; /** - * Enterprise Edition roles list with custom role management. - * Shows both predefined roles (read-only) and custom roles (CRUD). + * Enterprise Edition roles list with DataTable, search, and custom role management. */ export function RolesList() { - const { data: roles, isLoading } = useRoles(); + const searchParams = useSearchParams(); + const router = useRouter(); + const pathname = usePathname(); + + // URL-driven state + const pageFromUrl = searchParams.get("page"); + const pageSizeFromUrl = searchParams.get("pageSize"); + const searchFilter = searchParams.get("search") || ""; + const sortByFromUrl = searchParams.get("sortBy") as + | "name" + | "createdAt" + | null; + const sortDirectionFromUrl = searchParams.get("sortDirection") as + | "asc" + | "desc" + | null; + + const pageIndex = Number(pageFromUrl || "1") - 1; + const pageSize = Number(pageSizeFromUrl || DEFAULT_TABLE_LIMIT); + const offset = pageIndex * pageSize; + + const sortBy = sortByFromUrl || "createdAt"; + const sortDirection = sortDirectionFromUrl || "desc"; + + const { data: rolesResponse, isPending } = useRolesPaginated({ + limit: pageSize, + offset, + sortBy, + sortDirection, + search: searchFilter || undefined, + }); + const createMutation = useCreateRole(); const updateMutation = useUpdateRole(); const deleteMutation = useDeleteRole(); @@ -55,12 +88,52 @@ export function RolesList() { const [editDialogOpen, setEditDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [selectedRole, setSelectedRole] = useState(null); - const [roleToDelete, setRoleToDelete] = useState(null); + const [selectedRole, setSelectedRole] = useState(null); + const [roleToDelete, setRoleToDelete] = useState(null); const [roleName, setRoleName] = useState(""); + const [roleDescription, setRoleDescription] = useState(""); const [permission, setPermission] = useState({}); + const [sorting, setSorting] = useState([ + { id: sortBy, desc: sortDirection === "desc" }, + ]); + + // Sync sorting state with URL params + useEffect(() => { + setSorting([{ id: sortBy, desc: sortDirection === "desc" }]); + }, [sortBy, sortDirection]); + + const handleSortingChange = useCallback( + (updater: SortingState | ((old: SortingState) => SortingState)) => { + const newSorting = + typeof updater === "function" ? updater(sorting) : updater; + setSorting(newSorting); + + const params = new URLSearchParams(searchParams.toString()); + if (newSorting.length > 0) { + params.set("sortBy", newSorting[0].id); + params.set("sortDirection", newSorting[0].desc ? "desc" : "asc"); + } else { + params.delete("sortBy"); + params.delete("sortDirection"); + } + params.set("page", "1"); + router.push(`${pathname}?${params.toString()}`, { scroll: false }); + }, + [sorting, searchParams, router, pathname], + ); + + const handlePaginationChange = useCallback( + (newPagination: { pageIndex: number; pageSize: number }) => { + const params = new URLSearchParams(searchParams.toString()); + params.set("page", String(newPagination.pageIndex + 1)); + params.set("pageSize", String(newPagination.pageSize)); + router.push(`${pathname}?${params.toString()}`, { scroll: false }); + }, + [searchParams, router, pathname], + ); + const handleCreateRole = useCallback(() => { if (!roleName.trim()) { toast.error("Role name is required"); @@ -74,13 +147,16 @@ export function RolesList() { createMutation.mutate( // Cast needed: shared Permissions type includes "team-admin" before API types are regenerated - { name: roleName, permission } as Parameters< - typeof createMutation.mutate - >[0], + { + name: roleName, + description: roleDescription.trim() || null, + permission, + } as Parameters[0], { onSuccess: () => { setCreateDialogOpen(false); setRoleName(""); + setRoleDescription(""); setPermission({}); toast.success("Role created successfully"); }, @@ -89,7 +165,7 @@ export function RolesList() { }, }, ); - }, [roleName, permission, createMutation]); + }, [roleName, roleDescription, permission, createMutation]); const handleEditRole = useCallback(() => { if (!selectedRole) return; @@ -108,13 +184,18 @@ export function RolesList() { // Cast needed: shared Permissions type includes "team-admin" before API types are regenerated { roleId: selectedRole.id, - data: { name: roleName, permission }, + data: { + name: roleName, + description: roleDescription.trim() || null, + permission, + }, } as Parameters[0], { onSuccess: () => { setEditDialogOpen(false); setSelectedRole(null); setRoleName(""); + setRoleDescription(""); setPermission({}); toast.success("Role updated successfully"); }, @@ -123,7 +204,7 @@ export function RolesList() { }, }, ); - }, [selectedRole, roleName, permission, updateMutation]); + }, [selectedRole, roleName, roleDescription, permission, updateMutation]); const handleDeleteRole = useCallback(() => { if (roleToDelete) { @@ -140,240 +221,375 @@ export function RolesList() { } }, [roleToDelete, deleteMutation]); - const openEditDialog = useCallback((role: Role) => { + const openEditDialog = useCallback((role: RoleData) => { setSelectedRole(role); setRoleName(role.name); + setRoleDescription(role.description || ""); setPermission(role.permission); setEditDialogOpen(true); }, []); - if (isLoading) { - return ( - - - Roles - Loading roles... - - - ); - } + const roles = rolesResponse?.data || []; + const pagination = rolesResponse?.pagination; - const predefinedRoles = roles?.filter((role) => role.predefined) || []; - const customRoles = roles?.filter((role) => !role.predefined) || []; - - return ( - <> - - -
-
- Roles & Permissions - - Manage roles and their permissions. Custom roles can be created - with specific permission sets. -
- See documentation{" "} - - here - {" "} - for more information, including a complete list of available - permissions. -
-
+ const columns: ColumnDef[] = [ + { + id: "name", + accessorKey: "name", + size: 200, + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const role = row.original; + return ( +
+ {role.predefined && ( + + )} + {role.name} +
+ ); + }, + }, + { + id: "description", + accessorKey: "description", + header: "Description", + size: 300, + cell: ({ row }) => { + const role = row.original; + const description = role.predefined + ? roleDescriptions[role.name as PredefinedRoleName] || + role.description + : role.description; + return ( + + {description || "-"} + + ); + }, + }, + { + id: "type", + header: "Type", + size: 120, + cell: ({ row }) => { + const role = row.original; + return role.predefined ? ( + Predefined + ) : ( + Custom + ); + }, + }, + { + id: "createdAt", + accessorKey: "createdAt", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {formatDate({ date: row.original.createdAt })} +
+ ), + }, + { + id: "actions", + header: "Actions", + size: 120, + enableHiding: false, + cell: ({ row }) => { + const role = row.original; + if (role.predefined) { + return -; + } + return ( +
+ openEditDialog(role)} + > + Edit + setCreateDialogOpen(true)} + permissions={{ ac: ["delete"] }} + variant="outline" + size="sm" + onClick={() => { + setRoleToDelete(role); + setDeleteDialogOpen(true); + }} > - - Create Custom Role +
- - - - {customRoles.length > 0 && ( -
-

- Custom Roles -

-
- {customRoles.map((role) => ( -
-
- -
-

{role.name}

-
-
-
- openEditDialog(role)} - > - Edit - - { - setRoleToDelete(role); - setDeleteDialogOpen(true); - }} - > - - -
-
- ))} -
+ ); + }, + }, + ]; + + return ( + }> + + Manage roles and their permissions. Custom roles can be created with + specific permission sets.{" "} + + Read more in the docs + +

+ } + actionButton={ + setCreateDialogOpen(true)} + > + + Create Custom Role + + } + > +
+
+ +
+ + {!roles || roles.length === 0 ? ( +
+ {searchFilter + ? "No roles found matching your search" + : "No roles found"}
+ ) : ( + )} - - - - - - - Create Custom Role - - Create a new custom role with specific permissions. Users with - this role will only have access to the selected resources and - actions. - - - -
-
- - setRoleName(e.target.value)} - /> -
-
- - -
-
- - - - -
-
-
- - - - - Edit Role - - Modify the role name and permissions. Changes will affect all - users with this role. - - - -
-
- - setRoleName(e.target.value)} - /> +
+ + {/* Create Dialog */} + + + + Create Custom Role + + Create a new custom role with specific permissions. Users with + this role will only have access to the selected resources and + actions. + + + +
+
+ + setRoleName(e.target.value)} + /> +
+
+ +