diff --git a/.claude/commands/add-connector.md b/.claude/commands/add-connector.md new file mode 100644 index 000000000..a587d1642 --- /dev/null +++ b/.claude/commands/add-connector.md @@ -0,0 +1,240 @@ +--- +description: Add a knowledge base connector for syncing documents from an external source +argument-hint: [api-docs-url] +--- + +# Add Connector Skill + +You are an expert at adding knowledge base connectors to Sim. A connector syncs documents from an external source (Confluence, Google Drive, Notion, etc.) into a knowledge base. + +## Your Task + +When the user asks you to create a connector: +1. Use Context7 or WebFetch to read the service's API documentation +2. Create the connector directory and config +3. Register it in the connector registry + +## Directory Structure + +Create files in `apps/sim/connectors/{service}/`: +``` +connectors/{service}/ +├── index.ts # Barrel export +└── {service}.ts # ConnectorConfig definition +``` + +## ConnectorConfig Structure + +```typescript +import { createLogger } from '@sim/logger' +import { {Service}Icon } from '@/components/icons' +import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' + +const logger = createLogger('{Service}Connector') + +export const {service}Connector: ConnectorConfig = { + id: '{service}', + name: '{Service}', + description: 'Sync documents from {Service} into your knowledge base', + version: '1.0.0', + icon: {Service}Icon, + + oauth: { + required: true, + provider: '{service}', // Must match OAuthService in lib/oauth/types.ts + requiredScopes: ['read:...'], + }, + + configFields: [ + // Rendered dynamically by the add-connector modal UI + // Supports 'short-input' and 'dropdown' types + ], + + listDocuments: async (accessToken, sourceConfig, cursor) => { + // Paginate via cursor, extract text, compute SHA-256 hash + // Return { documents: ExternalDocument[], nextCursor?, hasMore } + }, + + getDocument: async (accessToken, sourceConfig, externalId) => { + // Return ExternalDocument or null + }, + + validateConfig: async (accessToken, sourceConfig) => { + // Return { valid: true } or { valid: false, error: 'message' } + }, + + // Optional: map source metadata to semantic tag keys (translated to slots by sync engine) + mapTags: (metadata) => { + // Return Record with keys matching tagDefinitions[].id + }, +} +``` + +## ConfigField Types + +The add-connector modal renders these automatically — no custom UI needed. + +```typescript +// Text input +{ + id: 'domain', + title: 'Domain', + type: 'short-input', + placeholder: 'yoursite.example.com', + required: true, +} + +// Dropdown (static options) +{ + id: 'contentType', + title: 'Content Type', + type: 'dropdown', + required: false, + options: [ + { label: 'Pages only', id: 'page' }, + { label: 'Blog posts only', id: 'blogpost' }, + { label: 'All content', id: 'all' }, + ], +} +``` + +## ExternalDocument Shape + +Every document returned from `listDocuments`/`getDocument` must include: + +```typescript +{ + externalId: string // Source-specific unique ID + title: string // Document title + content: string // Extracted plain text + mimeType: 'text/plain' // Always text/plain (content is extracted) + contentHash: string // SHA-256 of content (change detection) + sourceUrl?: string // Link back to original (stored on document record) + metadata?: Record // Source-specific data (fed to mapTags) +} +``` + +## Content Hashing (Required) + +The sync engine uses content hashes for change detection: + +```typescript +async function computeContentHash(content: string): Promise { + const data = new TextEncoder().encode(content) + const hashBuffer = await crypto.subtle.digest('SHA-256', data) + return Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('') +} +``` + +## tagDefinitions — Declared Tag Definitions + +Declare which tags the connector populates using semantic IDs. Shown in the add-connector modal as opt-out checkboxes. +On connector creation, slots are **dynamically assigned** via `getNextAvailableSlot` — connectors never hardcode slot names. + +```typescript +tagDefinitions: [ + { id: 'labels', displayName: 'Labels', fieldType: 'text' }, + { id: 'version', displayName: 'Version', fieldType: 'number' }, + { id: 'lastModified', displayName: 'Last Modified', fieldType: 'date' }, +], +``` + +Each entry has: +- `id`: Semantic key matching a key returned by `mapTags` (e.g. `'labels'`, `'version'`) +- `displayName`: Human-readable name shown in the UI (e.g. "Labels", "Last Modified") +- `fieldType`: `'text'` | `'number'` | `'date'` | `'boolean'` — determines which slot pool to draw from + +Users can opt out of specific tags in the modal. Disabled IDs are stored in `sourceConfig.disabledTagIds`. +The assigned mapping (`semantic id → slot`) is stored in `sourceConfig.tagSlotMapping`. + +## mapTags — Metadata to Semantic Keys + +Maps source metadata to semantic tag keys. Required if `tagDefinitions` is set. +The sync engine calls this automatically and translates semantic keys to actual DB slots +using the `tagSlotMapping` stored on the connector. + +Return keys must match the `id` values declared in `tagDefinitions`. + +```typescript +mapTags: (metadata: Record): Record => { + const result: Record = {} + + // Validate arrays before casting — metadata may be malformed + const labels = Array.isArray(metadata.labels) ? (metadata.labels as string[]) : [] + if (labels.length > 0) result.labels = labels.join(', ') + + // Validate numbers — guard against NaN + if (metadata.version != null) { + const num = Number(metadata.version) + if (!Number.isNaN(num)) result.version = num + } + + // Validate dates — guard against Invalid Date + if (typeof metadata.lastModified === 'string') { + const date = new Date(metadata.lastModified) + if (!Number.isNaN(date.getTime())) result.lastModified = date + } + + return result +} +``` + +## sourceUrl + +If `ExternalDocument.sourceUrl` is set, the sync engine stores it on the document record. Always construct the full URL (not a relative path). + +## Sync Engine Behavior (Do Not Modify) + +The sync engine (`lib/knowledge/connectors/sync-engine.ts`) is connector-agnostic. It: +1. Calls `listDocuments` with pagination until `hasMore` is false +2. Compares `contentHash` to detect new/changed/unchanged documents +3. Stores `sourceUrl` and calls `mapTags` on insert/update automatically +4. Handles soft-delete of removed documents + +You never need to modify the sync engine when adding a connector. + +## OAuth Credential Reuse + +Connectors reuse the existing OAuth infrastructure. The `oauth.provider` must match an `OAuthService` from `apps/sim/lib/oauth/types.ts`. Check existing providers before adding a new one. + +## Icon + +The `icon` field on `ConnectorConfig` is used throughout the UI — in the connector list, the add-connector modal, and as the document icon in the knowledge base table (replacing the generic file type icon for connector-sourced documents). The icon is read from `CONNECTOR_REGISTRY[connectorType].icon` at runtime — no separate icon map to maintain. + +If the service already has an icon in `apps/sim/components/icons.tsx` (from a tool integration), reuse it. Otherwise, ask the user to provide the SVG. + +## Registering + +Add one line to `apps/sim/connectors/registry.ts`: + +```typescript +import { {service}Connector } from '@/connectors/{service}' + +export const CONNECTOR_REGISTRY: ConnectorRegistry = { + // ... existing connectors ... + {service}: {service}Connector, +} +``` + +## Reference Implementation + +See `apps/sim/connectors/confluence/confluence.ts` for a complete example with: +- Multiple config field types (text + dropdown) +- Label fetching and CQL search filtering +- Blogpost + page content types +- `mapTags` mapping labels, version, and dates to semantic keys + +## Checklist + +- [ ] Created `connectors/{service}/{service}.ts` with full ConnectorConfig +- [ ] Created `connectors/{service}/index.ts` barrel export +- [ ] `oauth.provider` matches an existing OAuthService in `lib/oauth/types.ts` +- [ ] `listDocuments` handles pagination and computes content hashes +- [ ] `sourceUrl` set on each ExternalDocument (full URL, not relative) +- [ ] `metadata` includes source-specific data for tag mapping +- [ ] `tagDefinitions` declared for each semantic key returned by `mapTags` +- [ ] `mapTags` implemented if source has useful metadata (labels, dates, versions) +- [ ] `validateConfig` verifies the source is accessible +- [ ] All optional config fields validated in `validateConfig` +- [ ] Icon exists in `components/icons.tsx` (or asked user to provide SVG) +- [ ] Registered in `connectors/registry.ts` diff --git a/apps/docs/content/docs/en/tools/airtable.mdx b/apps/docs/content/docs/en/tools/airtable.mdx index 879b10b58..3150e3240 100644 --- a/apps/docs/content/docs/en/tools/airtable.mdx +++ b/apps/docs/content/docs/en/tools/airtable.mdx @@ -130,4 +130,37 @@ Update multiple existing records in an Airtable table | `records` | json | Array of updated Airtable records | | `metadata` | json | Operation metadata including record count and updated record IDs | +### `airtable_list_bases` + +List all bases the authenticated user has access to + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `bases` | json | Array of Airtable bases with id, name, and permissionLevel | +| `metadata` | json | Operation metadata including total bases count | + +### `airtable_get_base_schema` + +Get the schema of all tables, fields, and views in an Airtable base + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `baseId` | string | Yes | Airtable base ID \(starts with "app", e.g., "appXXXXXXXXXXXXXX"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tables` | json | Array of table schemas with fields and views | +| `metadata` | json | Operation metadata including total tables count | + diff --git a/apps/docs/content/docs/en/tools/knowledge.mdx b/apps/docs/content/docs/en/tools/knowledge.mdx index bc3b93e5c..be368aa6a 100644 --- a/apps/docs/content/docs/en/tools/knowledge.mdx +++ b/apps/docs/content/docs/en/tools/knowledge.mdx @@ -29,7 +29,7 @@ In Sim, the Knowledge Base block enables your agents to perform intelligent sema ## Usage Instructions -Integrate Knowledge into the workflow. Can search, upload chunks, and create documents. +Integrate Knowledge into the workflow. Perform full CRUD operations on documents, chunks, and tags. @@ -126,4 +126,161 @@ Create a new document in a knowledge base | `message` | string | Success or error message describing the operation result | | `documentId` | string | ID of the created document | +### `knowledge_list_tags` + +List all tag definitions for a knowledge base + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `knowledgeBaseId` | string | Yes | ID of the knowledge base to list tags for | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `knowledgeBaseId` | string | ID of the knowledge base | +| `tags` | array | Array of tag definitions for the knowledge base | +| ↳ `id` | string | Tag definition ID | +| ↳ `tagSlot` | string | Internal tag slot \(e.g. tag1, number1\) | +| ↳ `displayName` | string | Human-readable tag name | +| ↳ `fieldType` | string | Tag field type \(text, number, date, boolean\) | +| ↳ `createdAt` | string | Creation timestamp | +| ↳ `updatedAt` | string | Last update timestamp | +| `totalTags` | number | Total number of tag definitions | + +### `knowledge_list_documents` + +List documents in a knowledge base with optional filtering, search, and pagination + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `knowledgeBaseId` | string | Yes | ID of the knowledge base to list documents from | +| `search` | string | No | Search query to filter documents by filename | +| `enabledFilter` | string | No | Filter by enabled status: "all", "enabled", or "disabled" | +| `limit` | number | No | Maximum number of documents to return \(default: 50\) | +| `offset` | number | No | Number of documents to skip for pagination \(default: 0\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `knowledgeBaseId` | string | ID of the knowledge base | +| `documents` | array | Array of documents in the knowledge base | +| ↳ `id` | string | Document ID | +| ↳ `filename` | string | Document filename | +| ↳ `fileSize` | number | File size in bytes | +| ↳ `mimeType` | string | MIME type of the document | +| ↳ `enabled` | boolean | Whether the document is enabled | +| ↳ `processingStatus` | string | Processing status \(pending, processing, completed, failed\) | +| ↳ `chunkCount` | number | Number of chunks in the document | +| ↳ `tokenCount` | number | Total token count across chunks | +| ↳ `uploadedAt` | string | Upload timestamp | +| ↳ `updatedAt` | string | Last update timestamp | +| `totalDocuments` | number | Total number of documents matching the filter | +| `limit` | number | Page size used | +| `offset` | number | Offset used for pagination | + +### `knowledge_delete_document` + +Delete a document from a knowledge base + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `knowledgeBaseId` | string | Yes | ID of the knowledge base containing the document | +| `documentId` | string | Yes | ID of the document to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `documentId` | string | ID of the deleted document | +| `message` | string | Confirmation message | + +### `knowledge_list_chunks` + +List chunks for a document in a knowledge base with optional filtering and pagination + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `knowledgeBaseId` | string | Yes | ID of the knowledge base | +| `documentId` | string | Yes | ID of the document to list chunks from | +| `search` | string | No | Search query to filter chunks by content | +| `enabled` | string | No | Filter by enabled status: "true", "false", or "all" \(default: "all"\) | +| `limit` | number | No | Maximum number of chunks to return \(1-100, default: 50\) | +| `offset` | number | No | Number of chunks to skip for pagination \(default: 0\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `knowledgeBaseId` | string | ID of the knowledge base | +| `documentId` | string | ID of the document | +| `chunks` | array | Array of chunks in the document | +| ↳ `id` | string | Chunk ID | +| ↳ `chunkIndex` | number | Index of the chunk within the document | +| ↳ `content` | string | Chunk text content | +| ↳ `contentLength` | number | Content length in characters | +| ↳ `tokenCount` | number | Token count for the chunk | +| ↳ `enabled` | boolean | Whether the chunk is enabled | +| ↳ `createdAt` | string | Creation timestamp | +| ↳ `updatedAt` | string | Last update timestamp | +| `totalChunks` | number | Total number of chunks matching the filter | +| `limit` | number | Page size used | +| `offset` | number | Offset used for pagination | + +### `knowledge_update_chunk` + +Update the content or enabled status of a chunk in a knowledge base + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `knowledgeBaseId` | string | Yes | ID of the knowledge base | +| `documentId` | string | Yes | ID of the document containing the chunk | +| `chunkId` | string | Yes | ID of the chunk to update | +| `content` | string | No | New content for the chunk | +| `enabled` | boolean | No | Whether the chunk should be enabled or disabled | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `documentId` | string | ID of the parent document | +| `id` | string | Chunk ID | +| `chunkIndex` | number | Index of the chunk within the document | +| `content` | string | Updated chunk content | +| `contentLength` | number | Content length in characters | +| `tokenCount` | number | Token count for the chunk | +| `enabled` | boolean | Whether the chunk is enabled | +| `updatedAt` | string | Last update timestamp | + +### `knowledge_delete_chunk` + +Delete a chunk from a document in a knowledge base + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `knowledgeBaseId` | string | Yes | ID of the knowledge base | +| `documentId` | string | Yes | ID of the document containing the chunk | +| `chunkId` | string | Yes | ID of the chunk to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `chunkId` | string | ID of the deleted chunk | +| `documentId` | string | ID of the parent document | +| `message` | string | Confirmation message | + diff --git a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/documents/route.ts b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/documents/route.ts new file mode 100644 index 000000000..850000a68 --- /dev/null +++ b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/documents/route.ts @@ -0,0 +1,188 @@ +import { db } from '@sim/db' +import { document, knowledgeConnector } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, eq, inArray, isNull } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { checkKnowledgeBaseAccess, checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils' + +const logger = createLogger('ConnectorDocumentsAPI') + +type RouteParams = { params: Promise<{ id: string; connectorId: string }> } + +/** + * GET /api/knowledge/[id]/connectors/[connectorId]/documents + * Returns documents for a connector, optionally including user-excluded ones. + */ +export async function GET(request: NextRequest, { params }: RouteParams) { + const requestId = generateRequestId() + const { id: knowledgeBaseId, connectorId } = await params + + try { + const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, auth.userId) + if (!accessCheck.hasAccess) { + const status = 'notFound' in accessCheck && accessCheck.notFound ? 404 : 401 + return NextResponse.json({ error: status === 404 ? 'Not found' : 'Unauthorized' }, { status }) + } + + const connectorRows = await db + .select({ id: knowledgeConnector.id }) + .from(knowledgeConnector) + .where( + and( + eq(knowledgeConnector.id, connectorId), + eq(knowledgeConnector.knowledgeBaseId, knowledgeBaseId), + isNull(knowledgeConnector.deletedAt) + ) + ) + .limit(1) + + if (connectorRows.length === 0) { + return NextResponse.json({ error: 'Connector not found' }, { status: 404 }) + } + + const includeExcluded = request.nextUrl.searchParams.get('includeExcluded') === 'true' + + const activeDocs = await db + .select({ + id: document.id, + filename: document.filename, + externalId: document.externalId, + sourceUrl: document.sourceUrl, + enabled: document.enabled, + userExcluded: document.userExcluded, + uploadedAt: document.uploadedAt, + processingStatus: document.processingStatus, + }) + .from(document) + .where( + and( + eq(document.connectorId, connectorId), + isNull(document.deletedAt), + eq(document.userExcluded, false) + ) + ) + .orderBy(document.filename) + + const excludedDocs = includeExcluded + ? await db + .select({ + id: document.id, + filename: document.filename, + externalId: document.externalId, + sourceUrl: document.sourceUrl, + enabled: document.enabled, + userExcluded: document.userExcluded, + uploadedAt: document.uploadedAt, + processingStatus: document.processingStatus, + }) + .from(document) + .where(and(eq(document.connectorId, connectorId), eq(document.userExcluded, true))) + .orderBy(document.filename) + : [] + + const docs = [...activeDocs, ...excludedDocs] + const activeCount = activeDocs.length + const excludedCount = excludedDocs.length + + return NextResponse.json({ + success: true, + data: { + documents: docs, + counts: { active: activeCount, excluded: excludedCount }, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Error fetching connector documents`, error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} + +const PatchSchema = z.object({ + operation: z.enum(['restore', 'exclude']), + documentIds: z.array(z.string()).min(1), +}) + +/** + * PATCH /api/knowledge/[id]/connectors/[connectorId]/documents + * Restore or exclude connector documents. + */ +export async function PATCH(request: NextRequest, { params }: RouteParams) { + const requestId = generateRequestId() + const { id: knowledgeBaseId, connectorId } = await params + + try { + const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const writeCheck = await checkKnowledgeBaseWriteAccess(knowledgeBaseId, auth.userId) + if (!writeCheck.hasAccess) { + const status = 'notFound' in writeCheck && writeCheck.notFound ? 404 : 401 + return NextResponse.json({ error: status === 404 ? 'Not found' : 'Unauthorized' }, { status }) + } + + const body = await request.json() + const parsed = PatchSchema.safeParse(body) + if (!parsed.success) { + return NextResponse.json( + { error: 'Invalid request', details: parsed.error.flatten() }, + { status: 400 } + ) + } + + const { operation, documentIds } = parsed.data + + if (operation === 'restore') { + const updated = await db + .update(document) + .set({ userExcluded: false, deletedAt: null, enabled: true }) + .where( + and( + eq(document.connectorId, connectorId), + inArray(document.id, documentIds), + eq(document.userExcluded, true) + ) + ) + .returning({ id: document.id }) + + logger.info(`[${requestId}] Restored ${updated.length} excluded documents`, { connectorId }) + + return NextResponse.json({ + success: true, + data: { restoredCount: updated.length, documentIds: updated.map((d) => d.id) }, + }) + } + + const updated = await db + .update(document) + .set({ userExcluded: true, deletedAt: new Date() }) + .where( + and( + eq(document.connectorId, connectorId), + inArray(document.id, documentIds), + eq(document.userExcluded, false), + isNull(document.deletedAt) + ) + ) + .returning({ id: document.id }) + + logger.info(`[${requestId}] Excluded ${updated.length} documents`, { connectorId }) + + return NextResponse.json({ + success: true, + data: { excludedCount: updated.length, documentIds: updated.map((d) => d.id) }, + }) + } catch (error) { + logger.error(`[${requestId}] Error updating connector documents`, error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts new file mode 100644 index 000000000..1f15f37aa --- /dev/null +++ b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts @@ -0,0 +1,248 @@ +import { db } from '@sim/db' +import { knowledgeBase, knowledgeConnector, knowledgeConnectorSyncLog } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, desc, eq, isNull } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' +import { checkKnowledgeBaseAccess, checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils' +import { CONNECTOR_REGISTRY } from '@/connectors/registry' + +const logger = createLogger('KnowledgeConnectorByIdAPI') + +type RouteParams = { params: Promise<{ id: string; connectorId: string }> } + +const UpdateConnectorSchema = z.object({ + sourceConfig: z.record(z.unknown()).optional(), + syncIntervalMinutes: z.number().int().min(0).optional(), + status: z.enum(['active', 'paused']).optional(), +}) + +/** + * GET /api/knowledge/[id]/connectors/[connectorId] - Get connector details with recent sync logs + */ +export async function GET(request: NextRequest, { params }: RouteParams) { + const requestId = generateRequestId() + const { id: knowledgeBaseId, connectorId } = await params + + try { + const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, auth.userId) + if (!accessCheck.hasAccess) { + const status = 'notFound' in accessCheck && accessCheck.notFound ? 404 : 401 + return NextResponse.json({ error: status === 404 ? 'Not found' : 'Unauthorized' }, { status }) + } + + const connectorRows = await db + .select() + .from(knowledgeConnector) + .where( + and( + eq(knowledgeConnector.id, connectorId), + eq(knowledgeConnector.knowledgeBaseId, knowledgeBaseId), + isNull(knowledgeConnector.deletedAt) + ) + ) + .limit(1) + + if (connectorRows.length === 0) { + return NextResponse.json({ error: 'Connector not found' }, { status: 404 }) + } + + const syncLogs = await db + .select() + .from(knowledgeConnectorSyncLog) + .where(eq(knowledgeConnectorSyncLog.connectorId, connectorId)) + .orderBy(desc(knowledgeConnectorSyncLog.startedAt)) + .limit(10) + + return NextResponse.json({ + success: true, + data: { + ...connectorRows[0], + syncLogs, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Error fetching connector`, error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} + +/** + * PATCH /api/knowledge/[id]/connectors/[connectorId] - Update a connector + */ +export async function PATCH(request: NextRequest, { params }: RouteParams) { + const requestId = generateRequestId() + const { id: knowledgeBaseId, connectorId } = await params + + try { + const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const writeCheck = await checkKnowledgeBaseWriteAccess(knowledgeBaseId, auth.userId) + if (!writeCheck.hasAccess) { + const status = 'notFound' in writeCheck && writeCheck.notFound ? 404 : 401 + return NextResponse.json({ error: status === 404 ? 'Not found' : 'Unauthorized' }, { status }) + } + + const body = await request.json() + const parsed = UpdateConnectorSchema.safeParse(body) + if (!parsed.success) { + return NextResponse.json( + { error: 'Invalid request', details: parsed.error.flatten() }, + { status: 400 } + ) + } + + if (parsed.data.sourceConfig !== undefined) { + const existingRows = await db + .select() + .from(knowledgeConnector) + .where( + and( + eq(knowledgeConnector.id, connectorId), + eq(knowledgeConnector.knowledgeBaseId, knowledgeBaseId), + isNull(knowledgeConnector.deletedAt) + ) + ) + .limit(1) + + if (existingRows.length === 0) { + return NextResponse.json({ error: 'Connector not found' }, { status: 404 }) + } + + const existing = existingRows[0] + const connectorConfig = CONNECTOR_REGISTRY[existing.connectorType] + + if (!connectorConfig) { + return NextResponse.json( + { error: `Unknown connector type: ${existing.connectorType}` }, + { status: 400 } + ) + } + + const kbRows = await db + .select({ userId: knowledgeBase.userId }) + .from(knowledgeBase) + .where(eq(knowledgeBase.id, knowledgeBaseId)) + .limit(1) + + if (kbRows.length === 0) { + return NextResponse.json({ error: 'Knowledge base not found' }, { status: 404 }) + } + + const accessToken = await refreshAccessTokenIfNeeded( + existing.credentialId, + kbRows[0].userId, + `patch-${connectorId}` + ) + + if (!accessToken) { + return NextResponse.json( + { error: 'Failed to refresh access token. Please reconnect your account.' }, + { status: 401 } + ) + } + + const validation = await connectorConfig.validateConfig(accessToken, parsed.data.sourceConfig) + if (!validation.valid) { + return NextResponse.json( + { error: validation.error || 'Invalid source configuration' }, + { status: 400 } + ) + } + } + + const updates: Record = { updatedAt: new Date() } + if (parsed.data.sourceConfig !== undefined) { + updates.sourceConfig = parsed.data.sourceConfig + } + if (parsed.data.syncIntervalMinutes !== undefined) { + updates.syncIntervalMinutes = parsed.data.syncIntervalMinutes + if (parsed.data.syncIntervalMinutes > 0) { + updates.nextSyncAt = new Date(Date.now() + parsed.data.syncIntervalMinutes * 60 * 1000) + } else { + updates.nextSyncAt = null + } + } + if (parsed.data.status !== undefined) { + updates.status = parsed.data.status + } + + await db + .update(knowledgeConnector) + .set(updates) + .where( + and( + eq(knowledgeConnector.id, connectorId), + eq(knowledgeConnector.knowledgeBaseId, knowledgeBaseId), + isNull(knowledgeConnector.deletedAt) + ) + ) + + const updated = await db + .select() + .from(knowledgeConnector) + .where( + and( + eq(knowledgeConnector.id, connectorId), + eq(knowledgeConnector.knowledgeBaseId, knowledgeBaseId), + isNull(knowledgeConnector.deletedAt) + ) + ) + .limit(1) + + return NextResponse.json({ success: true, data: updated[0] }) + } catch (error) { + logger.error(`[${requestId}] Error updating connector`, error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} + +/** + * DELETE /api/knowledge/[id]/connectors/[connectorId] - Soft-delete a connector + */ +export async function DELETE(request: NextRequest, { params }: RouteParams) { + const requestId = generateRequestId() + const { id: knowledgeBaseId, connectorId } = await params + + try { + const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const writeCheck = await checkKnowledgeBaseWriteAccess(knowledgeBaseId, auth.userId) + if (!writeCheck.hasAccess) { + const status = 'notFound' in writeCheck && writeCheck.notFound ? 404 : 401 + return NextResponse.json({ error: status === 404 ? 'Not found' : 'Unauthorized' }, { status }) + } + + await db + .update(knowledgeConnector) + .set({ deletedAt: new Date(), status: 'paused', updatedAt: new Date() }) + .where( + and( + eq(knowledgeConnector.id, connectorId), + eq(knowledgeConnector.knowledgeBaseId, knowledgeBaseId), + isNull(knowledgeConnector.deletedAt) + ) + ) + + logger.info(`[${requestId}] Soft-deleted connector ${connectorId}`) + + return NextResponse.json({ success: true }) + } catch (error) { + logger.error(`[${requestId}] Error deleting connector`, error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/sync/route.ts b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/sync/route.ts new file mode 100644 index 000000000..b2f468994 --- /dev/null +++ b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/sync/route.ts @@ -0,0 +1,71 @@ +import { db } from '@sim/db' +import { knowledgeConnector } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, eq, isNull } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { dispatchSync } from '@/lib/knowledge/connectors/sync-engine' +import { checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils' + +const logger = createLogger('ConnectorManualSyncAPI') + +type RouteParams = { params: Promise<{ id: string; connectorId: string }> } + +/** + * POST /api/knowledge/[id]/connectors/[connectorId]/sync - Trigger a manual sync + */ +export async function POST(request: NextRequest, { params }: RouteParams) { + const requestId = generateRequestId() + const { id: knowledgeBaseId, connectorId } = await params + + try { + const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const writeCheck = await checkKnowledgeBaseWriteAccess(knowledgeBaseId, auth.userId) + if (!writeCheck.hasAccess) { + const status = 'notFound' in writeCheck && writeCheck.notFound ? 404 : 401 + return NextResponse.json({ error: status === 404 ? 'Not found' : 'Unauthorized' }, { status }) + } + + const connectorRows = await db + .select() + .from(knowledgeConnector) + .where( + and( + eq(knowledgeConnector.id, connectorId), + eq(knowledgeConnector.knowledgeBaseId, knowledgeBaseId), + isNull(knowledgeConnector.deletedAt) + ) + ) + .limit(1) + + if (connectorRows.length === 0) { + return NextResponse.json({ error: 'Connector not found' }, { status: 404 }) + } + + if (connectorRows[0].status === 'syncing') { + return NextResponse.json({ error: 'Sync already in progress' }, { status: 409 }) + } + + logger.info(`[${requestId}] Manual sync triggered for connector ${connectorId}`) + + dispatchSync(connectorId, { requestId }).catch((error) => { + logger.error( + `[${requestId}] Failed to dispatch manual sync for connector ${connectorId}`, + error + ) + }) + + return NextResponse.json({ + success: true, + message: 'Sync triggered', + }) + } catch (error) { + logger.error(`[${requestId}] Error triggering manual sync`, error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/knowledge/[id]/connectors/route.ts b/apps/sim/app/api/knowledge/[id]/connectors/route.ts new file mode 100644 index 000000000..9bd505ed8 --- /dev/null +++ b/apps/sim/app/api/knowledge/[id]/connectors/route.ts @@ -0,0 +1,193 @@ +import { db } from '@sim/db' +import { knowledgeConnector } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, desc, eq, isNull } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { dispatchSync } from '@/lib/knowledge/connectors/sync-engine' +import { createTagDefinition, getNextAvailableSlot } from '@/lib/knowledge/tags/service' +import { getCredential } from '@/app/api/auth/oauth/utils' +import { checkKnowledgeBaseAccess, checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils' +import { CONNECTOR_REGISTRY } from '@/connectors/registry' + +const logger = createLogger('KnowledgeConnectorsAPI') + +const CreateConnectorSchema = z.object({ + connectorType: z.string().min(1), + credentialId: z.string().min(1), + sourceConfig: z.record(z.unknown()), + syncIntervalMinutes: z.number().int().min(0).default(1440), +}) + +/** + * GET /api/knowledge/[id]/connectors - List connectors for a knowledge base + */ +export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const requestId = generateRequestId() + const { id: knowledgeBaseId } = await params + + try { + const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, auth.userId) + if (!accessCheck.hasAccess) { + const status = 'notFound' in accessCheck && accessCheck.notFound ? 404 : 401 + return NextResponse.json({ error: status === 404 ? 'Not found' : 'Unauthorized' }, { status }) + } + + const connectors = await db + .select() + .from(knowledgeConnector) + .where( + and( + eq(knowledgeConnector.knowledgeBaseId, knowledgeBaseId), + isNull(knowledgeConnector.deletedAt) + ) + ) + .orderBy(desc(knowledgeConnector.createdAt)) + + return NextResponse.json({ success: true, data: connectors }) + } catch (error) { + logger.error(`[${requestId}] Error listing connectors`, error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} + +/** + * POST /api/knowledge/[id]/connectors - Create a new connector + */ +export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const requestId = generateRequestId() + const { id: knowledgeBaseId } = await params + + try { + const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const writeCheck = await checkKnowledgeBaseWriteAccess(knowledgeBaseId, auth.userId) + if (!writeCheck.hasAccess) { + const status = 'notFound' in writeCheck && writeCheck.notFound ? 404 : 401 + return NextResponse.json({ error: status === 404 ? 'Not found' : 'Unauthorized' }, { status }) + } + + const body = await request.json() + const parsed = CreateConnectorSchema.safeParse(body) + if (!parsed.success) { + return NextResponse.json( + { error: 'Invalid request', details: parsed.error.flatten() }, + { status: 400 } + ) + } + + const { connectorType, credentialId, sourceConfig, syncIntervalMinutes } = parsed.data + + const connectorConfig = CONNECTOR_REGISTRY[connectorType] + if (!connectorConfig) { + return NextResponse.json( + { error: `Unknown connector type: ${connectorType}` }, + { status: 400 } + ) + } + + const credential = await getCredential(requestId, credentialId, auth.userId) + if (!credential) { + return NextResponse.json({ error: 'Credential not found' }, { status: 400 }) + } + + if (!credential.accessToken) { + return NextResponse.json( + { error: 'Credential has no access token. Please reconnect your account.' }, + { status: 400 } + ) + } + + const validation = await connectorConfig.validateConfig(credential.accessToken, sourceConfig) + if (!validation.valid) { + return NextResponse.json( + { error: validation.error || 'Invalid source configuration' }, + { status: 400 } + ) + } + + let finalSourceConfig: Record = sourceConfig + if (connectorConfig.tagDefinitions?.length) { + const disabledIds = new Set((sourceConfig.disabledTagIds as string[] | undefined) ?? []) + const enabledDefs = connectorConfig.tagDefinitions.filter((td) => !disabledIds.has(td.id)) + + const tagSlotMapping: Record = {} + const skippedTags: string[] = [] + for (const td of enabledDefs) { + const slot = await getNextAvailableSlot(knowledgeBaseId, td.fieldType) + if (!slot) { + skippedTags.push(td.displayName) + logger.warn(`[${requestId}] No available ${td.fieldType} slots for "${td.displayName}"`) + continue + } + await createTagDefinition( + { + knowledgeBaseId, + tagSlot: slot, + displayName: td.displayName, + fieldType: td.fieldType, + }, + requestId + ) + tagSlotMapping[td.id] = slot + } + + if (skippedTags.length > 0 && Object.keys(tagSlotMapping).length === 0) { + return NextResponse.json( + { error: `No available tag slots. Could not assign: ${skippedTags.join(', ')}` }, + { status: 422 } + ) + } + + finalSourceConfig = { ...sourceConfig, tagSlotMapping } + } + + const now = new Date() + const connectorId = crypto.randomUUID() + const nextSyncAt = + syncIntervalMinutes > 0 ? new Date(now.getTime() + syncIntervalMinutes * 60 * 1000) : null + + await db.insert(knowledgeConnector).values({ + id: connectorId, + knowledgeBaseId, + connectorType, + credentialId, + sourceConfig: finalSourceConfig, + syncIntervalMinutes, + status: 'active', + nextSyncAt, + createdAt: now, + updatedAt: now, + }) + + logger.info(`[${requestId}] Created connector ${connectorId} for KB ${knowledgeBaseId}`) + + dispatchSync(connectorId, { requestId }).catch((error) => { + logger.error( + `[${requestId}] Failed to dispatch initial sync for connector ${connectorId}`, + error + ) + }) + + const created = await db + .select() + .from(knowledgeConnector) + .where(eq(knowledgeConnector.id, connectorId)) + .limit(1) + + return NextResponse.json({ success: true, data: created[0] }, { status: 201 }) + } catch (error) { + logger.error(`[${requestId}] Error creating connector`, error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/knowledge/[id]/documents/route.ts b/apps/sim/app/api/knowledge/[id]/documents/route.ts index 30c1beafa..93fa9b437 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/route.ts @@ -12,6 +12,7 @@ import { getDocuments, getProcessingConfig, processDocumentsWithQueue, + type TagFilterCondition, } from '@/lib/knowledge/documents/service' import type { DocumentSortField, SortOrder } from '@/lib/knowledge/documents/types' import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' @@ -130,6 +131,21 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id: ? (sortOrderParam as SortOrder) : undefined + let tagFilters: TagFilterCondition[] | undefined + const tagFiltersParam = url.searchParams.get('tagFilters') + if (tagFiltersParam) { + try { + const parsed = JSON.parse(tagFiltersParam) + if (Array.isArray(parsed)) { + tagFilters = parsed.filter( + (f: TagFilterCondition) => f.tagSlot && f.operator && f.value !== undefined + ) + } + } catch { + logger.warn(`[${requestId}] Invalid tagFilters param`) + } + } + const result = await getDocuments( knowledgeBaseId, { @@ -139,6 +155,7 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id: offset, ...(sortBy && { sortBy }), ...(sortOrder && { sortOrder }), + tagFilters, }, requestId ) diff --git a/apps/sim/app/api/knowledge/connectors/sync/route.ts b/apps/sim/app/api/knowledge/connectors/sync/route.ts new file mode 100644 index 000000000..cc94b8209 --- /dev/null +++ b/apps/sim/app/api/knowledge/connectors/sync/route.ts @@ -0,0 +1,68 @@ +import { db } from '@sim/db' +import { knowledgeConnector } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, eq, isNull, lte } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { verifyCronAuth } from '@/lib/auth/internal' +import { generateRequestId } from '@/lib/core/utils/request' +import { dispatchSync } from '@/lib/knowledge/connectors/sync-engine' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('ConnectorSyncSchedulerAPI') + +/** + * Cron endpoint that checks for connectors due for sync and dispatches sync jobs. + * Should be called every 5 minutes by an external cron service. + */ +export async function GET(request: NextRequest) { + const requestId = generateRequestId() + logger.info(`[${requestId}] Connector sync scheduler triggered`) + + const authError = verifyCronAuth(request, 'Connector sync scheduler') + if (authError) { + return authError + } + + try { + const now = new Date() + + const dueConnectors = await db + .select({ + id: knowledgeConnector.id, + }) + .from(knowledgeConnector) + .where( + and( + eq(knowledgeConnector.status, 'active'), + lte(knowledgeConnector.nextSyncAt, now), + isNull(knowledgeConnector.deletedAt) + ) + ) + + logger.info(`[${requestId}] Found ${dueConnectors.length} connectors due for sync`) + + if (dueConnectors.length === 0) { + return NextResponse.json({ + success: true, + message: 'No connectors due for sync', + count: 0, + }) + } + + for (const connector of dueConnectors) { + dispatchSync(connector.id, { requestId }).catch((error) => { + logger.error(`[${requestId}] Failed to dispatch sync for connector ${connector.id}`, error) + }) + } + + return NextResponse.json({ + success: true, + message: `Dispatched ${dueConnectors.length} connector sync(s)`, + count: dueConnectors.length, + }) + } catch (error) { + logger.error(`[${requestId}] Connector sync scheduler error`, error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/create-chunk-modal/create-chunk-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/create-chunk-modal/create-chunk-modal.tsx index 0a9125f92..ebb603ebb 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/create-chunk-modal/create-chunk-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/create-chunk-modal/create-chunk-modal.tsx @@ -13,7 +13,7 @@ import { Textarea, } from '@/components/emcn' import type { DocumentData } from '@/lib/knowledge/types' -import { useCreateChunk } from '@/hooks/queries/knowledge' +import { useCreateChunk } from '@/hooks/queries/kb/knowledge' const logger = createLogger('CreateChunkModal') diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/delete-chunk-modal/delete-chunk-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/delete-chunk-modal/delete-chunk-modal.tsx index fcebce6b8..2fdba7a80 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/delete-chunk-modal/delete-chunk-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/delete-chunk-modal/delete-chunk-modal.tsx @@ -2,7 +2,7 @@ import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@/components/emcn' import type { ChunkData } from '@/lib/knowledge/types' -import { useDeleteChunk } from '@/hooks/queries/knowledge' +import { useDeleteChunk } from '@/hooks/queries/kb/knowledge' interface DeleteChunkModalProps { chunk: ChunkData | null diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/document-tags-modal/document-tags-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/document-tags-modal/document-tags-modal.tsx index 13c01e223..7ea01a8af 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/document-tags-modal/document-tags-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/document-tags-modal/document-tags-modal.tsx @@ -25,7 +25,7 @@ import { } from '@/hooks/kb/use-knowledge-base-tag-definitions' import { useNextAvailableSlot } from '@/hooks/kb/use-next-available-slot' import { type TagDefinitionInput, useTagDefinitions } from '@/hooks/kb/use-tag-definitions' -import { useUpdateDocumentTags } from '@/hooks/queries/knowledge' +import { useUpdateDocumentTags } from '@/hooks/queries/kb/knowledge' const logger = createLogger('DocumentTagsModal') diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/edit-chunk-modal/edit-chunk-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/edit-chunk-modal/edit-chunk-modal.tsx index e1faef399..643faa53a 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/edit-chunk-modal/edit-chunk-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/edit-chunk-modal/edit-chunk-modal.tsx @@ -18,7 +18,7 @@ import { import type { ChunkData, DocumentData } from '@/lib/knowledge/types' import { getAccurateTokenCount, getTokenStrings } from '@/lib/tokenization/estimators' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' -import { useUpdateChunk } from '@/hooks/queries/knowledge' +import { useUpdateChunk } from '@/hooks/queries/kb/knowledge' const logger = createLogger('EditChunkModal') diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx index ad4750e85..eda60181e 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx @@ -4,6 +4,7 @@ import { startTransition, useCallback, useEffect, useRef, useState } from 'react import { createLogger } from '@sim/logger' import { useQueryClient } from '@tanstack/react-query' import { + ChevronDown, ChevronLeft, ChevronRight, Circle, @@ -24,6 +25,10 @@ import { ModalContent, ModalFooter, ModalHeader, + Popover, + PopoverContent, + PopoverItem, + PopoverTrigger, Table, TableBody, TableCell, @@ -55,7 +60,7 @@ import { useDeleteDocument, useDocumentChunkSearchQuery, useUpdateChunk, -} from '@/hooks/queries/knowledge' +} from '@/hooks/queries/kb/knowledge' const logger = createLogger('Document') @@ -256,6 +261,8 @@ export function Document({ const [searchQuery, setSearchQuery] = useState('') const [debouncedSearchQuery, setDebouncedSearchQuery] = useState('') + const [enabledFilter, setEnabledFilter] = useState<'all' | 'enabled' | 'disabled'>('all') + const [isFilterPopoverOpen, setIsFilterPopoverOpen] = useState(false) const { chunks: initialChunks, @@ -268,7 +275,7 @@ export function Document({ refreshChunks: initialRefreshChunks, updateChunk: initialUpdateChunk, isFetching: isFetchingChunks, - } = useDocumentChunks(knowledgeBaseId, documentId, currentPageFromURL) + } = useDocumentChunks(knowledgeBaseId, documentId, currentPageFromURL, '', enabledFilter) const { data: searchResults = [], @@ -690,47 +697,100 @@ export function Document({
-
- - setSearchQuery(e.target.value)} - disabled={documentData?.processingStatus !== 'completed'} - className='flex-1 border-0 bg-transparent px-0 font-medium text-[var(--text-secondary)] text-small leading-none placeholder:text-[var(--text-subtle)] focus-visible:ring-0 focus-visible:ring-offset-0' - /> - {searchQuery && - (isLoadingSearch ? ( - - ) : ( - - ))} +
+
+ + setSearchQuery(e.target.value)} + disabled={documentData?.processingStatus !== 'completed'} + className='flex-1 border-0 bg-transparent px-0 font-medium text-[var(--text-secondary)] text-small leading-none placeholder:text-[var(--text-subtle)] focus-visible:ring-0 focus-visible:ring-offset-0' + /> + {searchQuery && + (isLoadingSearch ? ( + + ) : ( + + ))} +
- - - - - {!userPermissions.canEdit && ( - Write permission required to create chunks - )} - +
+ + + + + +
+ { + setEnabledFilter('all') + setIsFilterPopoverOpen(false) + setSelectedChunks(new Set()) + }} + > + All + + { + setEnabledFilter('enabled') + setIsFilterPopoverOpen(false) + setSelectedChunks(new Set()) + }} + > + Enabled + + { + setEnabledFilter('disabled') + setIsFilterPopoverOpen(false) + setSelectedChunks(new Set()) + }} + > + Disabled + +
+
+
+ + + + + + {!userPermissions.canEdit && ( + Write permission required to create chunks + )} + +
- {/* Delete Document Modal */} Delete Document @@ -1072,7 +1131,14 @@ export function Document({ ? This will permanently delete the document and all {documentData?.chunkCount ?? 0}{' '} chunk {documentData?.chunkCount === 1 ? '' : 's'} within it.{' '} - This action cannot be undone. + {documentData?.connectorId ? ( + + This document is synced from a connector. Deleting it will permanently exclude it + from future syncs. To temporarily hide it from search, disable it instead. + + ) : ( + This action cannot be undone. + )}

diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx index 15d1d36d2..af0b33222 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx @@ -1,6 +1,6 @@ 'use client' -import { useCallback, useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' import { format } from 'date-fns' import { @@ -11,7 +11,9 @@ import { ChevronUp, Circle, CircleOff, + Filter, Loader2, + Plus, RotateCcw, Search, X, @@ -22,6 +24,11 @@ import { Breadcrumb, Button, Checkbox, + Combobox, + type ComboboxOption, + DatePicker, + Input, + Label, Modal, ModalBody, ModalContent, @@ -40,25 +47,28 @@ import { Tooltip, Trash, } from '@/components/emcn' -import { Input } from '@/components/ui/input' import { SearchHighlight } from '@/components/ui/search-highlight' import { Skeleton } from '@/components/ui/skeleton' import { cn } from '@/lib/core/utils/cn' import { formatAbsoluteDate, formatRelativeTime } from '@/lib/core/utils/formatting' import { ALL_TAG_SLOTS, type AllTagSlot, getFieldTypeForSlot } from '@/lib/knowledge/constants' import type { DocumentSortField, SortOrder } from '@/lib/knowledge/documents/types' +import { type FilterFieldType, getOperatorsForFieldType } from '@/lib/knowledge/filters/types' import type { DocumentData } from '@/lib/knowledge/types' import { formatFileSize } from '@/lib/uploads/utils/file-utils' import { ActionBar, + AddConnectorModal, AddDocumentsModal, BaseTagsModal, + ConnectorsSection, DocumentContextMenu, RenameDocumentModal, } from '@/app/workspace/[workspaceId]/knowledge/[id]/components' import { getDocumentIcon } from '@/app/workspace/[workspaceId]/knowledge/components' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks' +import { CONNECTOR_REGISTRY } from '@/connectors/registry' import { useKnowledgeBase, useKnowledgeBaseDocuments, @@ -68,12 +78,14 @@ import { type TagDefinition, useKnowledgeBaseTagDefinitions, } from '@/hooks/kb/use-knowledge-base-tag-definitions' +import { useConnectorList } from '@/hooks/queries/kb/connectors' +import type { DocumentTagFilter } from '@/hooks/queries/kb/knowledge' import { useBulkDocumentOperation, useDeleteDocument, useDeleteKnowledgeBase, useUpdateDocument, -} from '@/hooks/queries/knowledge' +} from '@/hooks/queries/kb/knowledge' const logger = createLogger('KnowledgeBase') @@ -345,6 +357,32 @@ export function KnowledgeBase({ const [showTagsModal, setShowTagsModal] = useState(false) const [enabledFilter, setEnabledFilter] = useState<'all' | 'enabled' | 'disabled'>('all') const [isFilterPopoverOpen, setIsFilterPopoverOpen] = useState(false) + const [isTagFilterPopoverOpen, setIsTagFilterPopoverOpen] = useState(false) + const [tagFilterEntries, setTagFilterEntries] = useState< + { + id: string + tagName: string + tagSlot: string + fieldType: FilterFieldType + operator: string + value: string + valueTo: string + }[] + >([]) + + const activeTagFilters: DocumentTagFilter[] = useMemo( + () => + tagFilterEntries + .filter((f) => f.tagSlot && f.value.trim()) + .map((f) => ({ + tagSlot: f.tagSlot, + fieldType: f.fieldType, + operator: f.operator, + value: f.value, + ...(f.operator === 'between' && f.valueTo ? { valueTo: f.valueTo } : {}), + })), + [tagFilterEntries] + ) /** * Memoize the search query setter to prevent unnecessary re-renders @@ -367,6 +405,7 @@ export function KnowledgeBase({ const [contextMenuDocument, setContextMenuDocument] = useState(null) const [showRenameModal, setShowRenameModal] = useState(false) const [documentToRename, setDocumentToRename] = useState(null) + const [showAddConnectorModal, setShowAddConnectorModal] = useState(false) const { isOpen: isContextMenuOpen, @@ -407,10 +446,23 @@ export function KnowledgeBase({ return hasPending ? 3000 : false }, enabledFilter, + tagFilters: activeTagFilters.length > 0 ? activeTagFilters : undefined, }) const { tagDefinitions } = useKnowledgeBaseTagDefinitions(id) + const { data: connectors = [], isLoading: isLoadingConnectors } = useConnectorList(id) + const hasSyncingConnectors = connectors.some((c) => c.status === 'syncing') + + /** Refresh KB detail when connectors transition from syncing to done */ + const prevHadSyncingRef = useRef(false) + useEffect(() => { + if (prevHadSyncingRef.current && !hasSyncingConnectors) { + refreshKnowledgeBase() + } + prevHadSyncingRef.current = hasSyncingConnectors + }, [hasSyncingConnectors, refreshKnowledgeBase]) + const router = useRouter() const knowledgeBaseName = knowledgeBase?.name || passedKnowledgeBaseName || 'Knowledge Base' @@ -1003,6 +1055,14 @@ export function KnowledgeBase({ )}
+ setShowAddConnectorModal(true)} + /> +
{pagination.total} {pagination.total === 1 ? 'document' : 'documents'} @@ -1049,7 +1109,7 @@ export function KnowledgeBase({
) } + +interface TagFilterEntry { + id: string + tagName: string + tagSlot: string + fieldType: FilterFieldType + operator: string + value: string + valueTo: string +} + +const createEmptyEntry = (): TagFilterEntry => ({ + id: crypto.randomUUID(), + tagName: '', + tagSlot: '', + fieldType: 'text', + operator: 'eq', + value: '', + valueTo: '', +}) + +interface TagFilterPopoverProps { + tagDefinitions: TagDefinition[] + entries: TagFilterEntry[] + isOpen: boolean + onOpenChange: (open: boolean) => void + onChange: (entries: TagFilterEntry[]) => void +} + +function TagFilterPopover({ + tagDefinitions, + entries, + isOpen, + onOpenChange, + onChange, +}: TagFilterPopoverProps) { + const activeCount = entries.filter((f) => f.tagSlot && f.value.trim()).length + + const tagOptions: ComboboxOption[] = tagDefinitions.map((t) => ({ + value: t.displayName, + label: t.displayName, + })) + + const filtersToShow = useMemo( + () => (entries.length > 0 ? entries : [createEmptyEntry()]), + [entries] + ) + + const updateEntry = (id: string, patch: Partial) => { + const existing = filtersToShow.find((e) => e.id === id) + if (!existing) return + const updated = filtersToShow.map((e) => (e.id === id ? { ...e, ...patch } : e)) + onChange(updated) + } + + const handleTagChange = (id: string, tagName: string) => { + const def = tagDefinitions.find((t) => t.displayName === tagName) + const fieldType = (def?.fieldType || 'text') as FilterFieldType + const operators = getOperatorsForFieldType(fieldType) + updateEntry(id, { + tagName, + tagSlot: def?.tagSlot || '', + fieldType, + operator: operators[0]?.value || 'eq', + value: '', + valueTo: '', + }) + } + + const addFilter = () => { + onChange([...filtersToShow, createEmptyEntry()]) + } + + const removeFilter = (id: string) => { + const remaining = filtersToShow.filter((e) => e.id !== id) + onChange(remaining.length > 0 ? remaining : []) + } + + return ( + + + + + +
+
+ + Filter by tags + +
+ {activeCount > 0 && ( + + )} + +
+
+ +
+ {filtersToShow.map((entry) => { + const operators = getOperatorsForFieldType(entry.fieldType) + const operatorOptions: ComboboxOption[] = operators.map((op) => ({ + value: op.value, + label: op.label, + })) + const isBetween = entry.operator === 'between' + + return ( +
+
+ + +
+ handleTagChange(entry.id, v)} + placeholder='Select tag' + /> + + {entry.tagSlot && ( + <> + + updateEntry(entry.id, { operator: v, valueTo: '' })} + placeholder='Select operator' + /> + + + {entry.fieldType === 'date' ? ( + isBetween ? ( +
+ updateEntry(entry.id, { value: v })} + placeholder='From' + /> + + to + + updateEntry(entry.id, { valueTo: v })} + placeholder='To' + /> +
+ ) : ( + updateEntry(entry.id, { value: v })} + placeholder='Select date' + /> + ) + ) : isBetween ? ( +
+ updateEntry(entry.id, { value: e.target.value })} + placeholder='From' + className='h-[28px] text-[12px]' + /> + + to + + updateEntry(entry.id, { valueTo: e.target.value })} + placeholder='To' + className='h-[28px] text-[12px]' + /> +
+ ) : ( + updateEntry(entry.id, { value: e.target.value })} + placeholder={ + entry.fieldType === 'boolean' + ? 'true or false' + : entry.fieldType === 'number' + ? 'Enter number' + : 'Enter value' + } + className='h-[28px] text-[12px]' + /> + )} + + )} +
+ ) + })} +
+
+
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx new file mode 100644 index 000000000..5f8b54da5 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx @@ -0,0 +1,348 @@ +'use client' + +import { useMemo, useState } from 'react' +import { ArrowLeft, Loader2, Plus } from 'lucide-react' +import { + Button, + ButtonGroup, + ButtonGroupItem, + Checkbox, + Combobox, + type ComboboxOption, + Input, + Label, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, +} from '@/components/emcn' +import { + getCanonicalScopesForProvider, + getProviderIdFromServiceId, + type OAuthProvider, +} from '@/lib/oauth' +import { OAuthRequiredModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal' +import { CONNECTOR_REGISTRY } from '@/connectors/registry' +import type { ConnectorConfig } from '@/connectors/types' +import { useCreateConnector } from '@/hooks/queries/kb/connectors' +import { useOAuthCredentials } from '@/hooks/queries/oauth/oauth-credentials' + +const SYNC_INTERVALS = [ + { label: 'Every hour', value: 60 }, + { label: 'Every 6 hours', value: 360 }, + { label: 'Daily', value: 1440 }, + { label: 'Weekly', value: 10080 }, + { label: 'Manual only', value: 0 }, +] as const + +interface AddConnectorModalProps { + open: boolean + onOpenChange: (open: boolean) => void + knowledgeBaseId: string +} + +type Step = 'select-type' | 'configure' + +export function AddConnectorModal({ open, onOpenChange, knowledgeBaseId }: AddConnectorModalProps) { + const [step, setStep] = useState('select-type') + const [selectedType, setSelectedType] = useState(null) + const [sourceConfig, setSourceConfig] = useState>({}) + const [syncInterval, setSyncInterval] = useState(1440) + const [selectedCredentialId, setSelectedCredentialId] = useState(null) + const [disabledTagIds, setDisabledTagIds] = useState>(new Set()) + const [error, setError] = useState(null) + const [showOAuthModal, setShowOAuthModal] = useState(false) + + const { mutate: createConnector, isPending: isCreating } = useCreateConnector() + + const connectorConfig = selectedType ? CONNECTOR_REGISTRY[selectedType] : null + const connectorProviderId = useMemo( + () => + connectorConfig + ? (getProviderIdFromServiceId(connectorConfig.oauth.provider) as OAuthProvider) + : null, + [connectorConfig] + ) + + const { data: credentials = [], isLoading: credentialsLoading } = useOAuthCredentials( + connectorConfig?.oauth.provider, + Boolean(connectorConfig) + ) + + const effectiveCredentialId = + selectedCredentialId ?? (credentials.length === 1 ? credentials[0].id : null) + + const handleSelectType = (type: string) => { + setSelectedType(type) + setStep('configure') + } + + const canSubmit = useMemo(() => { + if (!connectorConfig || !effectiveCredentialId) return false + return connectorConfig.configFields + .filter((f) => f.required) + .every((f) => sourceConfig[f.id]?.trim()) + }, [connectorConfig, effectiveCredentialId, sourceConfig]) + + const handleSubmit = () => { + if (!selectedType || !effectiveCredentialId || !canSubmit) return + + setError(null) + + const finalSourceConfig = + disabledTagIds.size > 0 + ? { ...sourceConfig, disabledTagIds: Array.from(disabledTagIds) } + : sourceConfig + + createConnector( + { + knowledgeBaseId, + connectorType: selectedType, + credentialId: effectiveCredentialId, + sourceConfig: finalSourceConfig, + syncIntervalMinutes: syncInterval, + }, + { + onSuccess: () => { + onOpenChange(false) + }, + onError: (err) => { + setError(err.message) + }, + } + ) + } + + const connectorEntries = Object.entries(CONNECTOR_REGISTRY) + + return ( + <> + !isCreating && onOpenChange(val)}> + + + {step === 'configure' && ( + + )} + {step === 'select-type' ? 'Connect Source' : `Configure ${connectorConfig?.name}`} + + + + {step === 'select-type' ? ( +
+ {connectorEntries.map(([type, config]) => ( + handleSelectType(type)} + /> + ))} + {connectorEntries.length === 0 && ( +

No connectors available.

+ )} +
+ ) : connectorConfig ? ( +
+ {/* Credential selection */} +
+ + {credentialsLoading ? ( +
+ + Loading credentials... +
+ ) : ( + ({ + label: cred.name || cred.provider, + value: cred.id, + icon: connectorConfig.icon, + }) + ), + { + label: 'Connect new account', + value: '__connect_new__', + icon: Plus, + onSelect: () => { + setShowOAuthModal(true) + }, + }, + ]} + value={effectiveCredentialId ?? undefined} + onChange={(value) => setSelectedCredentialId(value)} + placeholder={ + credentials.length === 0 + ? `No ${connectorConfig.name} accounts` + : 'Select account' + } + /> + )} +
+ + {/* Config fields */} + {connectorConfig.configFields.map((field) => ( +
+ + {field.description && ( +

{field.description}

+ )} + {field.type === 'dropdown' && field.options ? ( + ({ + label: opt.label, + value: opt.id, + }))} + value={sourceConfig[field.id] || undefined} + onChange={(value) => + setSourceConfig((prev) => ({ ...prev, [field.id]: value })) + } + placeholder={field.placeholder || `Select ${field.title.toLowerCase()}`} + /> + ) : ( + + setSourceConfig((prev) => ({ ...prev, [field.id]: e.target.value })) + } + placeholder={field.placeholder} + /> + )} +
+ ))} + + {/* Tag definitions (opt-out) */} + {connectorConfig.tagDefinitions && connectorConfig.tagDefinitions.length > 0 && ( +
+ + {connectorConfig.tagDefinitions.map((tagDef) => ( +
{ + setDisabledTagIds((prev) => { + const next = new Set(prev) + if (prev.has(tagDef.id)) { + next.delete(tagDef.id) + } else { + next.add(tagDef.id) + } + return next + }) + }} + > + { + setDisabledTagIds((prev) => { + const next = new Set(prev) + if (checked) { + next.delete(tagDef.id) + } else { + next.add(tagDef.id) + } + return next + }) + }} + /> + {tagDef.displayName} + + ({tagDef.fieldType}) + +
+ ))} +
+ )} + + {/* Sync interval */} +
+ + setSyncInterval(Number(val))} + > + {SYNC_INTERVALS.map((interval) => ( + + {interval.label} + + ))} + +
+ + {error && ( +

{error}

+ )} +
+ ) : null} +
+ + {step === 'configure' && ( + + + + + )} +
+
+ {connectorConfig && connectorProviderId && ( + setShowOAuthModal(false)} + provider={connectorProviderId} + toolName={connectorConfig.name} + requiredScopes={getCanonicalScopesForProvider(connectorProviderId)} + newScopes={connectorConfig.oauth.requiredScopes || []} + serviceId={connectorConfig.oauth.provider} + /> + )} + + ) +} + +interface ConnectorTypeCardProps { + config: ConnectorConfig + onClick: () => void +} + +function ConnectorTypeCard({ config, onClick }: ConnectorTypeCardProps) { + const Icon = config.icon + + return ( + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/base-tags-modal/base-tags-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/base-tags-modal/base-tags-modal.tsx index 282a85622..265f75203 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/base-tags-modal/base-tags-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/base-tags-modal/base-tags-modal.tsx @@ -22,7 +22,7 @@ import { type TagDefinition, useKnowledgeBaseTagDefinitions, } from '@/hooks/kb/use-knowledge-base-tag-definitions' -import { useCreateTagDefinition, useDeleteTagDefinition } from '@/hooks/queries/knowledge' +import { useCreateTagDefinition, useDeleteTagDefinition } from '@/hooks/queries/kb/knowledge' const logger = createLogger('BaseTagsModal') diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx new file mode 100644 index 000000000..0dd5d5f3b --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx @@ -0,0 +1,539 @@ +'use client' + +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { createLogger } from '@sim/logger' +import { format, formatDistanceToNow } from 'date-fns' +import { + AlertCircle, + CheckCircle2, + ChevronDown, + Loader2, + Pause, + Play, + RefreshCw, + Settings, + Trash, + Unplug, + XCircle, +} from 'lucide-react' +import { + Badge, + Button, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + Tooltip, +} from '@/components/emcn' +import { cn } from '@/lib/core/utils/cn' +import { + getCanonicalScopesForProvider, + getProviderIdFromServiceId, + type OAuthProvider, +} from '@/lib/oauth' +import { EditConnectorModal } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/edit-connector-modal' +import { OAuthRequiredModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal' +import { CONNECTOR_META } from '@/connectors/icons' +import { CONNECTOR_REGISTRY } from '@/connectors/registry' +import type { ConnectorData, SyncLogData } from '@/hooks/queries/kb/connectors' +import { + useConnectorDetail, + useDeleteConnector, + useTriggerSync, + useUpdateConnector, +} from '@/hooks/queries/kb/connectors' +import { useOAuthCredentials } from '@/hooks/queries/oauth/oauth-credentials' +import { getMissingRequiredScopes } from '@/hooks/use-oauth-scope-status' + +const logger = createLogger('ConnectorsSection') + +interface ConnectorsSectionProps { + knowledgeBaseId: string + connectors: ConnectorData[] + isLoading: boolean + canEdit: boolean + onAddConnector: () => void +} + +/** 5-minute cooldown after a manual sync trigger */ +const SYNC_COOLDOWN_MS = 5 * 60 * 1000 + +const STATUS_CONFIG = { + active: { label: 'Active', variant: 'green' as const }, + syncing: { label: 'Syncing', variant: 'amber' as const }, + error: { label: 'Error', variant: 'red' as const }, + paused: { label: 'Paused', variant: 'gray' as const }, +} as const + +export function ConnectorsSection({ + knowledgeBaseId, + connectors, + isLoading, + canEdit, + onAddConnector, +}: ConnectorsSectionProps) { + const { mutate: triggerSync, isPending: isSyncing } = useTriggerSync() + const { mutate: updateConnector } = useUpdateConnector() + const { mutate: deleteConnector } = useDeleteConnector() + const [deleteTarget, setDeleteTarget] = useState(null) + const [editingConnector, setEditingConnector] = useState(null) + const [error, setError] = useState(null) + + const syncTriggeredAt = useRef>({}) + const cooldownTimers = useRef>>(new Set()) + const [, forceUpdate] = useState(0) + + useEffect(() => { + return () => { + for (const timer of cooldownTimers.current) { + clearTimeout(timer) + } + } + }, []) + + const isSyncOnCooldown = useCallback((connectorId: string) => { + const triggeredAt = syncTriggeredAt.current[connectorId] + if (!triggeredAt) return false + return Date.now() - triggeredAt < SYNC_COOLDOWN_MS + }, []) + + const handleSync = useCallback( + (connectorId: string) => { + if (isSyncOnCooldown(connectorId)) return + + syncTriggeredAt.current[connectorId] = Date.now() + + triggerSync( + { knowledgeBaseId, connectorId }, + { + onSuccess: () => { + setError(null) + const timer = setTimeout(() => { + cooldownTimers.current.delete(timer) + forceUpdate((n) => n + 1) + }, SYNC_COOLDOWN_MS) + cooldownTimers.current.add(timer) + }, + onError: (err) => { + logger.error('Sync trigger failed', { error: err.message }) + setError(err.message) + delete syncTriggeredAt.current[connectorId] + forceUpdate((n) => n + 1) + }, + } + ) + }, + [knowledgeBaseId, triggerSync, isSyncOnCooldown] + ) + + if (isLoading) return null + if (connectors.length === 0 && !canEdit) return null + + return ( +
+
+

Connected Sources

+ {canEdit && ( + + )} +
+ + {error && ( +

{error}

+ )} + + {connectors.length === 0 ? ( +

+ No connected sources yet. Connect an external source to automatically sync documents. +

+ ) : ( +
+ {connectors.map((connector) => ( + handleSync(connector.id)} + onTogglePause={() => + updateConnector( + { + knowledgeBaseId, + connectorId: connector.id, + updates: { + status: connector.status === 'paused' ? 'active' : 'paused', + }, + }, + { + onSuccess: () => setError(null), + onError: (err) => { + logger.error('Toggle pause failed', { error: err.message }) + setError(err.message) + }, + } + ) + } + onEdit={() => setEditingConnector(connector)} + onDelete={() => setDeleteTarget(connector.id)} + /> + ))} +
+ )} + + {editingConnector && ( + !val && setEditingConnector(null)} + knowledgeBaseId={knowledgeBaseId} + connector={editingConnector} + /> + )} + + setDeleteTarget(null)}> + + Delete Connector + +

+ Are you sure you want to remove this connected source? Documents already synced will + remain in the knowledge base. +

+
+ + + + +
+
+
+ ) +} + +interface ConnectorCardProps { + connector: ConnectorData + knowledgeBaseId: string + canEdit: boolean + isSyncing: boolean + syncCooldown: boolean + onSync: () => void + onEdit: () => void + onTogglePause: () => void + onDelete: () => void +} + +function ConnectorCard({ + connector, + knowledgeBaseId, + canEdit, + isSyncing, + syncCooldown, + onSync, + onEdit, + onTogglePause, + onDelete, +}: ConnectorCardProps) { + const [expanded, setExpanded] = useState(false) + const [showOAuthModal, setShowOAuthModal] = useState(false) + + const meta = CONNECTOR_META[connector.connectorType] + const Icon = meta?.icon + const statusConfig = + STATUS_CONFIG[connector.status as keyof typeof STATUS_CONFIG] || STATUS_CONFIG.active + + const connectorConfig = CONNECTOR_REGISTRY[connector.connectorType] + const serviceId = connectorConfig?.oauth.provider + const providerId = serviceId ? getProviderIdFromServiceId(serviceId) : undefined + const requiredScopes = connectorConfig?.oauth.requiredScopes ?? [] + + const { data: credentials } = useOAuthCredentials(providerId) + + const missingScopes = useMemo(() => { + if (!credentials || !connector.credentialId) return [] + const credential = credentials.find((c) => c.id === connector.credentialId) + return getMissingRequiredScopes(credential, requiredScopes) + }, [credentials, connector.credentialId, requiredScopes]) + + const { data: detail, isLoading: detailLoading } = useConnectorDetail( + expanded ? knowledgeBaseId : undefined, + expanded ? connector.id : undefined + ) + const syncLogs = detail?.syncLogs ?? [] + + return ( +
+
+
+ {Icon && } +
+
+ + {meta?.name || connector.connectorType} + + + {connector.status === 'syncing' && ( + + )} + {statusConfig.label} + +
+
+ {connector.lastSyncAt && ( + Last sync: {format(new Date(connector.lastSyncAt), 'MMM d, h:mm a')} + )} + {connector.lastSyncDocCount !== null && ( + <> + · + {connector.lastSyncDocCount} docs + + )} + {connector.nextSyncAt && connector.status === 'active' && ( + <> + · + + Next sync:{' '} + {formatDistanceToNow(new Date(connector.nextSyncAt), { addSuffix: true })} + + + )} + {connector.lastSyncError && ( + + + + + {connector.lastSyncError} + + )} +
+
+
+ +
+ {canEdit && ( + <> + + + + + + {syncCooldown ? 'Sync recently triggered' : 'Sync now'} + + + + + + + + Settings + + + + + + + + {connector.status === 'paused' ? 'Resume' : 'Pause'} + + + + + + + + Delete + + + )} + + + + + + {expanded ? 'Hide history' : 'Sync history'} + +
+
+ + {missingScopes.length > 0 && ( +
+
+
+ + Additional permissions required +
+ {canEdit && ( + + )} +
+
+ )} + + {expanded && ( +
+ +
+ )} + + {showOAuthModal && serviceId && providerId && ( + setShowOAuthModal(false)} + provider={providerId as OAuthProvider} + toolName={connectorConfig?.name ?? connector.connectorType} + requiredScopes={getCanonicalScopesForProvider(providerId)} + newScopes={missingScopes} + serviceId={serviceId} + /> + )} +
+ ) +} + +interface SyncHistoryProps { + logs: SyncLogData[] + isLoading: boolean +} + +function SyncHistory({ logs, isLoading }: SyncHistoryProps) { + if (isLoading) { + return ( +
+ + Loading sync history... +
+ ) + } + + if (logs.length === 0) { + return

No sync history yet.

+ } + + return ( +
+ {logs.map((log) => { + const isError = log.status === 'error' || log.status === 'failed' + const isRunning = log.status === 'running' || log.status === 'syncing' + const totalChanges = log.docsAdded + log.docsUpdated + log.docsDeleted + + return ( +
+
+ {isRunning ? ( + + ) : isError ? ( + + ) : ( + + )} +
+ +
+
+ + {format(new Date(log.startedAt), 'MMM d, h:mm a')} + + {!isRunning && !isError && ( + + {totalChanges > 0 ? ( + <> + {log.docsAdded > 0 && ( + +{log.docsAdded} + )} + {log.docsUpdated > 0 && ( + <> + {log.docsAdded > 0 && ' '} + + ~{log.docsUpdated} + + + )} + {log.docsDeleted > 0 && ( + <> + {(log.docsAdded > 0 || log.docsUpdated > 0) && ' '} + -{log.docsDeleted} + + )} + + ) : ( + 'No changes' + )} + + )} + {isRunning && In progress...} +
+ + {isError && log.errorMessage && ( + {log.errorMessage} + )} +
+
+ ) + })} +
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-context-menu/document-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-context-menu/document-context-menu.tsx index e747e5ea5..1acfad08b 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-context-menu/document-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/document-context-menu/document-context-menu.tsx @@ -17,6 +17,7 @@ interface DocumentContextMenuProps { * Document-specific actions (shown when right-clicking on a document) */ onOpenInNewTab?: () => void + onOpenSource?: () => void onRename?: () => void onToggleEnabled?: () => void onViewTags?: () => void @@ -74,6 +75,7 @@ export function DocumentContextMenu({ menuRef, onClose, onOpenInNewTab, + onOpenSource, onRename, onToggleEnabled, onViewTags, @@ -129,7 +131,17 @@ export function DocumentContextMenu({ Open in new tab )} - {!isMultiSelect && onOpenInNewTab && } + {!isMultiSelect && onOpenSource && ( + { + onOpenSource() + onClose() + }} + > + Open source + + )} + {!isMultiSelect && (onOpenInNewTab || onOpenSource) && } {/* Edit and view actions */} {!isMultiSelect && onRename && ( @@ -170,6 +182,7 @@ export function DocumentContextMenu({ {/* Destructive action */} {onDelete && ((!isMultiSelect && onOpenInNewTab) || + (!isMultiSelect && onOpenSource) || (!isMultiSelect && onRename) || (!isMultiSelect && hasTags && onViewTags) || onToggleEnabled) && } diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/edit-connector-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/edit-connector-modal.tsx new file mode 100644 index 000000000..27ad0b3ef --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/edit-connector-modal.tsx @@ -0,0 +1,339 @@ +'use client' + +import { useMemo, useState } from 'react' +import { createLogger } from '@sim/logger' +import { ExternalLink, Loader2, RotateCcw } from 'lucide-react' +import { + Button, + ButtonGroup, + ButtonGroupItem, + Combobox, + Input, + Label, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + ModalTabs, + ModalTabsContent, + ModalTabsList, + ModalTabsTrigger, +} from '@/components/emcn' +import { Skeleton } from '@/components/ui/skeleton' +import { CONNECTOR_REGISTRY } from '@/connectors/registry' +import type { ConnectorConfig } from '@/connectors/types' +import type { ConnectorData } from '@/hooks/queries/kb/connectors' +import { + useConnectorDocuments, + useExcludeConnectorDocument, + useRestoreConnectorDocument, + useUpdateConnector, +} from '@/hooks/queries/kb/connectors' + +const logger = createLogger('EditConnectorModal') + +const SYNC_INTERVALS = [ + { label: 'Every hour', value: 60 }, + { label: 'Every 6 hours', value: 360 }, + { label: 'Daily', value: 1440 }, + { label: 'Weekly', value: 10080 }, + { label: 'Manual only', value: 0 }, +] as const + +/** Keys injected by the sync engine — not user-editable */ +const INTERNAL_CONFIG_KEYS = new Set(['tagSlotMapping', 'disabledTagIds']) + +interface EditConnectorModalProps { + open: boolean + onOpenChange: (open: boolean) => void + knowledgeBaseId: string + connector: ConnectorData +} + +export function EditConnectorModal({ + open, + onOpenChange, + knowledgeBaseId, + connector, +}: EditConnectorModalProps) { + const connectorConfig = CONNECTOR_REGISTRY[connector.connectorType] ?? null + + const initialSourceConfig = useMemo(() => { + const config: Record = {} + for (const [key, value] of Object.entries(connector.sourceConfig)) { + if (!INTERNAL_CONFIG_KEYS.has(key)) { + config[key] = String(value ?? '') + } + } + return config + }, [connector.sourceConfig]) + + const [activeTab, setActiveTab] = useState('settings') + const [sourceConfig, setSourceConfig] = useState>(initialSourceConfig) + const [syncInterval, setSyncInterval] = useState(connector.syncIntervalMinutes) + const [error, setError] = useState(null) + + const { mutate: updateConnector, isPending: isSaving } = useUpdateConnector() + + const hasChanges = useMemo(() => { + if (syncInterval !== connector.syncIntervalMinutes) return true + for (const [key, value] of Object.entries(sourceConfig)) { + if (String(connector.sourceConfig[key] ?? '') !== value) return true + } + return false + }, [sourceConfig, syncInterval, connector.syncIntervalMinutes, connector.sourceConfig]) + + const handleSave = () => { + setError(null) + + const updates: { sourceConfig?: Record; syncIntervalMinutes?: number } = {} + + if (syncInterval !== connector.syncIntervalMinutes) { + updates.syncIntervalMinutes = syncInterval + } + + const configChanged = Object.entries(sourceConfig).some( + ([key, value]) => String(connector.sourceConfig[key] ?? '') !== value + ) + if (configChanged) { + updates.sourceConfig = { ...connector.sourceConfig, ...sourceConfig } + } + + if (Object.keys(updates).length === 0) { + onOpenChange(false) + return + } + + updateConnector( + { knowledgeBaseId, connectorId: connector.id, updates }, + { + onSuccess: () => { + onOpenChange(false) + }, + onError: (err) => { + logger.error('Failed to update connector', { error: err.message }) + setError(err.message) + }, + } + ) + } + + const displayName = connectorConfig?.name ?? connector.connectorType + const Icon = connectorConfig?.icon + + return ( + !isSaving && onOpenChange(val)}> + + +
+ {Icon && } + Edit {displayName} +
+
+ + + + Settings + Documents + + + + + + + + + + + + + + {activeTab === 'settings' && ( + + + + + )} +
+
+ ) +} + +interface SettingsTabProps { + connectorConfig: ConnectorConfig | null + sourceConfig: Record + setSourceConfig: React.Dispatch>> + syncInterval: number + setSyncInterval: (v: number) => void + error: string | null +} + +function SettingsTab({ + connectorConfig, + sourceConfig, + setSourceConfig, + syncInterval, + setSyncInterval, + error, +}: SettingsTabProps) { + return ( +
+ {connectorConfig?.configFields.map((field) => ( +
+ + {field.description && ( +

{field.description}

+ )} + {field.type === 'dropdown' && field.options ? ( + ({ + label: opt.label, + value: opt.id, + }))} + value={sourceConfig[field.id] || undefined} + onChange={(value) => setSourceConfig((prev) => ({ ...prev, [field.id]: value }))} + placeholder={field.placeholder || `Select ${field.title.toLowerCase()}`} + /> + ) : ( + setSourceConfig((prev) => ({ ...prev, [field.id]: e.target.value }))} + placeholder={field.placeholder} + /> + )} +
+ ))} + +
+ + setSyncInterval(Number(val))} + > + {SYNC_INTERVALS.map((interval) => ( + + {interval.label} + + ))} + +
+ + {error &&

{error}

} +
+ ) +} + +interface DocumentsTabProps { + knowledgeBaseId: string + connectorId: string +} + +function DocumentsTab({ knowledgeBaseId, connectorId }: DocumentsTabProps) { + const [filter, setFilter] = useState<'active' | 'excluded'>('active') + + const { data, isLoading } = useConnectorDocuments(knowledgeBaseId, connectorId, { + includeExcluded: true, + }) + + const { mutate: excludeDoc, isPending: isExcluding } = useExcludeConnectorDocument() + const { mutate: restoreDoc, isPending: isRestoring } = useRestoreConnectorDocument() + + const documents = useMemo(() => { + if (!data?.documents) return [] + return data.documents.filter((d) => (filter === 'excluded' ? d.userExcluded : !d.userExcluded)) + }, [data?.documents, filter]) + + const counts = data?.counts ?? { active: 0, excluded: 0 } + + if (isLoading) { + return ( +
+ + + +
+ ) + } + + return ( +
+ setFilter(val as 'active' | 'excluded')}> + Active ({counts.active}) + Excluded ({counts.excluded}) + + +
+ {documents.length === 0 ? ( +

+ {filter === 'excluded' ? 'No excluded documents' : 'No documents yet'} +

+ ) : ( +
+ {documents.map((doc) => ( +
+
+ + {doc.filename} + + {doc.sourceUrl && ( + + + + )} +
+ +
+ ))} +
+ )} +
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/index.ts b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/index.ts new file mode 100644 index 000000000..12a78407d --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/index.ts @@ -0,0 +1 @@ +export { EditConnectorModal } from './edit-connector-modal' diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/index.ts b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/index.ts index b0e8ca143..552727116 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/index.ts @@ -1,5 +1,8 @@ export { ActionBar } from './action-bar/action-bar' +export { AddConnectorModal } from './add-connector-modal/add-connector-modal' export { AddDocumentsModal } from './add-documents-modal/add-documents-modal' export { BaseTagsModal } from './base-tags-modal/base-tags-modal' +export { ConnectorsSection } from './connectors-section/connectors-section' export { DocumentContextMenu } from './document-context-menu' +export { EditConnectorModal } from './edit-connector-modal/edit-connector-modal' export { RenameDocumentModal } from './rename-document-modal' diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/base-card/base-card.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/base-card/base-card.tsx index a213b7431..0155b7963 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/components/base-card/base-card.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/base-card/base-card.tsx @@ -7,6 +7,7 @@ import { formatAbsoluteDate, formatRelativeTime } from '@/lib/core/utils/formatt import { BaseTagsModal } from '@/app/workspace/[workspaceId]/knowledge/[id]/components' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks' +import { CONNECTOR_REGISTRY } from '@/connectors/registry' import { DeleteKnowledgeBaseModal } from '../delete-knowledge-base-modal/delete-knowledge-base-modal' import { EditKnowledgeBaseModal } from '../edit-knowledge-base-modal/edit-knowledge-base-modal' import { KnowledgeBaseContextMenu } from '../knowledge-base-context-menu/knowledge-base-context-menu' @@ -18,6 +19,7 @@ interface BaseCardProps { description: string createdAt?: string updatedAt?: string + connectorTypes?: string[] onUpdate?: (id: string, name: string, description: string) => Promise onDelete?: (id: string) => Promise } @@ -75,6 +77,7 @@ export function BaseCard({ docCount, description, updatedAt, + connectorTypes = [], onUpdate, onDelete, }: BaseCardProps) { @@ -200,9 +203,33 @@ export function BaseCard({
-

- {description} -

+
+

+ {description} +

+ {connectorTypes.length > 0 && ( +
+ {connectorTypes.map((type, index) => { + const config = CONNECTOR_REGISTRY[type] + if (!config?.icon) return null + const Icon = config.icon + return ( + + +
0 ? '-4px' : '0' }} + > + +
+
+ {config.name} +
+ ) + })} +
+ )} +
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-base-modal/create-base-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-base-modal/create-base-modal.tsx index 70419c821..d6bc4a35e 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-base-modal/create-base-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-base-modal/create-base-modal.tsx @@ -22,7 +22,7 @@ import { cn } from '@/lib/core/utils/cn' import { formatFileSize, validateKnowledgeBaseFile } from '@/lib/uploads/utils/file-utils' import { ACCEPT_ATTRIBUTE } from '@/lib/uploads/utils/validation' import { useKnowledgeUpload } from '@/app/workspace/[workspaceId]/knowledge/hooks/use-knowledge-upload' -import { useCreateKnowledgeBase, useDeleteKnowledgeBase } from '@/hooks/queries/knowledge' +import { useCreateKnowledgeBase, useDeleteKnowledgeBase } from '@/hooks/queries/kb/knowledge' const logger = createLogger('CreateBaseModal') diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-header/knowledge-header.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-header/knowledge-header.tsx index 4ae936af7..8bdcc6e3c 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-header/knowledge-header.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-header/knowledge-header.tsx @@ -14,7 +14,7 @@ import { } from '@/components/emcn' import { Trash } from '@/components/emcn/icons/trash' import { filterButtonClass } from '@/app/workspace/[workspaceId]/knowledge/components/constants' -import { useUpdateKnowledgeBase } from '@/hooks/queries/knowledge' +import { useUpdateKnowledgeBase } from '@/hooks/queries/kb/knowledge' const logger = createLogger('KnowledgeHeader') diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/hooks/use-knowledge-upload.ts b/apps/sim/app/workspace/[workspaceId]/knowledge/hooks/use-knowledge-upload.ts index 818990e17..5dcc75ef4 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/hooks/use-knowledge-upload.ts +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/hooks/use-knowledge-upload.ts @@ -2,7 +2,7 @@ import { useCallback, useState } from 'react' import { createLogger } from '@sim/logger' import { useQueryClient } from '@tanstack/react-query' import { getFileExtension, getMimeTypeFromExtension } from '@/lib/uploads/utils/file-utils' -import { knowledgeKeys } from '@/hooks/queries/knowledge' +import { knowledgeKeys } from '@/hooks/queries/kb/knowledge' const logger = createLogger('KnowledgeUpload') diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx index 0be7e8f61..aba0ecfd7 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx @@ -32,7 +32,7 @@ import { import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks' import { useKnowledgeBasesList } from '@/hooks/kb/use-knowledge' -import { useDeleteKnowledgeBase, useUpdateKnowledgeBase } from '@/hooks/queries/knowledge' +import { useDeleteKnowledgeBase, useUpdateKnowledgeBase } from '@/hooks/queries/kb/knowledge' import { useDebounce } from '@/hooks/use-debounce' const logger = createLogger('Knowledge') @@ -153,6 +153,7 @@ export function Knowledge() { description: kb.description || 'No description provided', createdAt: kb.createdAt, updatedAt: kb.updatedAt, + connectorTypes: kb.connectorTypes ?? [], }) /** @@ -283,6 +284,7 @@ export function Knowledge() { title={displayData.title} docCount={displayData.docCount} description={displayData.description} + connectorTypes={displayData.connectorTypes} createdAt={displayData.createdAt} updatedAt={displayData.updatedAt} onUpdate={handleUpdateKnowledgeBase} diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/notifications.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/notifications.tsx index e1dcc4193..275c030e9 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/notifications.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/notifications.tsx @@ -32,7 +32,10 @@ import { useTestNotification, useUpdateNotification, } from '@/hooks/queries/notifications' -import { useConnectedAccounts, useConnectOAuthService } from '@/hooks/queries/oauth-connections' +import { + useConnectedAccounts, + useConnectOAuthService, +} from '@/hooks/queries/oauth/oauth-connections' import { CORE_TRIGGER_TYPES, type CoreTriggerType } from '@/stores/logs/filters/types' import { SlackChannelSelector } from './components/slack-channel-selector' import { WorkflowSelector } from './components/workflow-selector' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx index 4888a9684..cb2946d66 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx @@ -95,6 +95,7 @@ const SCOPE_DESCRIPTIONS: Record = { 'offline.access': 'Access account when not using the application', 'data.records:read': 'Read records', 'data.records:write': 'Write to records', + 'schema.bases:read': 'Read base schemas and table metadata', 'webhook:manage': 'Manage webhooks', 'page.read': 'Read Notion pages', 'page.write': 'Write to Notion pages', diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx index 378a9baed..94b3054b4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx @@ -20,7 +20,10 @@ import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/c import type { SubBlockConfig } from '@/blocks/types' import { CREDENTIAL, CREDENTIAL_SET } from '@/executor/constants' import { useCredentialSets } from '@/hooks/queries/credential-sets' -import { useOAuthCredentialDetail, useOAuthCredentials } from '@/hooks/queries/oauth-credentials' +import { + useOAuthCredentialDetail, + useOAuthCredentials, +} from '@/hooks/queries/oauth/oauth-credentials' import { useOrganizations } from '@/hooks/queries/organization' import { useSubscriptionData } from '@/hooks/queries/subscription' import { getMissingRequiredScopes } from '@/hooks/use-oauth-scope-status' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-base-selector/knowledge-base-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-base-selector/knowledge-base-selector.tsx index dc9bf4d5d..4f45aee71 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-base-selector/knowledge-base-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-base-selector/knowledge-base-selector.tsx @@ -10,7 +10,7 @@ import type { KnowledgeBaseData } from '@/lib/knowledge/types' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import type { SubBlockConfig } from '@/blocks/types' import { useKnowledgeBasesList } from '@/hooks/kb/use-knowledge' -import { fetchKnowledgeBase, knowledgeKeys } from '@/hooks/queries/knowledge' +import { fetchKnowledgeBase, knowledgeKeys } from '@/hooks/queries/kb/knowledge' interface KnowledgeBaseSelectorProps { blockId: string diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/credential-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/credential-selector.tsx index 255d85907..00ad71b0c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/credential-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/credential-selector.tsx @@ -12,7 +12,10 @@ import { } from '@/lib/oauth' import { OAuthRequiredModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal' import { CREDENTIAL } from '@/executor/constants' -import { useOAuthCredentialDetail, useOAuthCredentials } from '@/hooks/queries/oauth-credentials' +import { + useOAuthCredentialDetail, + useOAuthCredentials, +} from '@/hooks/queries/oauth/oauth-credentials' import { getMissingRequiredScopes } from '@/hooks/use-oauth-scope-status' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index c0f89e2b3..446ed8b77 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -38,7 +38,7 @@ import { getDependsOnFields } from '@/blocks/utils' import { useKnowledgeBase } from '@/hooks/kb/use-knowledge' import { useCustomTools } from '@/hooks/queries/custom-tools' import { useMcpServers, useMcpToolsQuery } from '@/hooks/queries/mcp' -import { useCredentialName } from '@/hooks/queries/oauth-credentials' +import { useCredentialName } from '@/hooks/queries/oauth/oauth-credentials' import { useReactivateSchedule, useScheduleInfo } from '@/hooks/queries/schedules' import { useSkills } from '@/hooks/queries/skills' import { useDeployChildWorkflow } from '@/hooks/queries/workflows' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/integrations/integrations.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/integrations/integrations.tsx index dabdfc03f..aeeb2dba8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/integrations/integrations.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/integrations/integrations.tsx @@ -21,7 +21,7 @@ import { useConnectOAuthService, useDisconnectOAuthService, useOAuthConnections, -} from '@/hooks/queries/oauth-connections' +} from '@/hooks/queries/oauth/oauth-connections' import { usePermissionConfig } from '@/hooks/use-permission-config' const logger = createLogger('Integrations') diff --git a/apps/sim/background/knowledge-connector-sync.ts b/apps/sim/background/knowledge-connector-sync.ts new file mode 100644 index 000000000..20ea13c1d --- /dev/null +++ b/apps/sim/background/knowledge-connector-sync.ts @@ -0,0 +1,53 @@ +import { createLogger } from '@sim/logger' +import { task } from '@trigger.dev/sdk' +import { executeSync } from '@/lib/knowledge/connectors/sync-engine' + +const logger = createLogger('TriggerKnowledgeConnectorSync') + +export type ConnectorSyncPayload = { + connectorId: string + fullSync?: boolean + requestId: string +} + +export const knowledgeConnectorSync = task({ + id: 'knowledge-connector-sync', + maxDuration: 1800, + machine: 'large-1x', + retry: { + maxAttempts: 3, + factor: 2, + minTimeoutInMs: 5000, + maxTimeoutInMs: 30000, + }, + queue: { + concurrencyLimit: 5, + name: 'connector-sync-queue', + }, + run: async (payload: ConnectorSyncPayload) => { + const { connectorId, fullSync, requestId } = payload + + logger.info(`[${requestId}] Starting connector sync: ${connectorId}`) + + try { + const result = await executeSync(connectorId, { fullSync }) + + logger.info(`[${requestId}] Connector sync completed`, { + connectorId, + added: result.docsAdded, + updated: result.docsUpdated, + deleted: result.docsDeleted, + unchanged: result.docsUnchanged, + }) + + return { + success: !result.error, + connectorId, + ...result, + } + } catch (error) { + logger.error(`[${requestId}] Connector sync failed: ${connectorId}`, error) + throw error + } + }, +}) diff --git a/apps/sim/blocks/blocks/airtable.ts b/apps/sim/blocks/blocks/airtable.ts index 502032c87..85b0c3b51 100644 --- a/apps/sim/blocks/blocks/airtable.ts +++ b/apps/sim/blocks/blocks/airtable.ts @@ -25,6 +25,8 @@ export const AirtableBlock: BlockConfig = { { label: 'Get Record', id: 'get' }, { label: 'Create Records', id: 'create' }, { label: 'Update Record', id: 'update' }, + { label: 'List Bases', id: 'listBases' }, + { label: 'Get Base Schema', id: 'getSchema' }, ], value: () => 'list', }, @@ -36,6 +38,7 @@ export const AirtableBlock: BlockConfig = { requiredScopes: [ 'data.records:read', 'data.records:write', + 'schema.bases:read', 'user.email:read', 'webhook:manage', ], @@ -48,6 +51,7 @@ export const AirtableBlock: BlockConfig = { type: 'short-input', placeholder: 'Enter your base ID (e.g., appXXXXXXXXXXXXXX)', dependsOn: ['credential'], + condition: { field: 'operation', value: 'listBases', not: true }, required: true, }, { @@ -56,6 +60,7 @@ export const AirtableBlock: BlockConfig = { type: 'short-input', placeholder: 'Enter table ID (e.g., tblXXXXXXXXXXXXXX)', dependsOn: ['credential', 'baseId'], + condition: { field: 'operation', value: ['listBases', 'getSchema'], not: true }, required: true, }, { @@ -200,6 +205,8 @@ Return ONLY the valid JSON object - no explanations, no markdown.`, 'airtable_create_records', 'airtable_update_record', 'airtable_update_multiple_records', + 'airtable_list_bases', + 'airtable_get_base_schema', ], config: { tool: (params) => { @@ -214,6 +221,10 @@ Return ONLY the valid JSON object - no explanations, no markdown.`, return 'airtable_update_record' case 'updateMultiple': return 'airtable_update_multiple_records' + case 'listBases': + return 'airtable_list_bases' + case 'getSchema': + return 'airtable_get_base_schema' default: throw new Error(`Invalid Airtable operation: ${params.operation}`) } @@ -267,9 +278,11 @@ Return ONLY the valid JSON object - no explanations, no markdown.`, }, // Output structure depends on the operation, covered by AirtableResponse union type outputs: { - records: { type: 'json', description: 'Retrieved record data' }, // Optional: for list, create, updateMultiple - record: { type: 'json', description: 'Single record data' }, // Optional: for get, update single - metadata: { type: 'json', description: 'Operation metadata' }, // Required: present in all responses + records: { type: 'json', description: 'Retrieved record data' }, + record: { type: 'json', description: 'Single record data' }, + bases: { type: 'json', description: 'List of accessible Airtable bases' }, + tables: { type: 'json', description: 'Table schemas with fields and views' }, + metadata: { type: 'json', description: 'Operation metadata' }, // Trigger outputs event_type: { type: 'string', description: 'Type of Airtable event' }, base_id: { type: 'string', description: 'Airtable base identifier' }, diff --git a/apps/sim/blocks/blocks/knowledge.ts b/apps/sim/blocks/blocks/knowledge.ts index 722c4d7d8..196769de5 100644 --- a/apps/sim/blocks/blocks/knowledge.ts +++ b/apps/sim/blocks/blocks/knowledge.ts @@ -6,9 +6,11 @@ export const KnowledgeBlock: BlockConfig = { name: 'Knowledge', description: 'Use vector search', longDescription: - 'Integrate Knowledge into the workflow. Can search, upload chunks, and create documents.', + 'Integrate Knowledge into the workflow. Perform full CRUD operations on documents, chunks, and tags.', bestPractices: ` - Clarify which tags are available for the knowledge base to understand whether to use tag filters on a search. + - Use List Documents to enumerate documents before operating on them. + - Use List Chunks to inspect a document's contents before updating or deleting chunks. `, bgColor: '#00B0B0', icon: PackageSearchIcon, @@ -21,20 +23,41 @@ export const KnowledgeBlock: BlockConfig = { type: 'dropdown', options: [ { label: 'Search', id: 'search' }, - { label: 'Upload Chunk', id: 'upload_chunk' }, + { label: 'List Documents', id: 'list_documents' }, { label: 'Create Document', id: 'create_document' }, + { label: 'Delete Document', id: 'delete_document' }, + { label: 'List Chunks', id: 'list_chunks' }, + { label: 'Upload Chunk', id: 'upload_chunk' }, + { label: 'Update Chunk', id: 'update_chunk' }, + { label: 'Delete Chunk', id: 'delete_chunk' }, + { label: 'List Tags', id: 'list_tags' }, ], value: () => 'search', }, + + // Knowledge Base selector — basic mode (visual selector) { - id: 'knowledgeBaseId', + id: 'knowledgeBaseSelector', title: 'Knowledge Base', type: 'knowledge-base-selector', + canonicalParamId: 'knowledgeBaseId', placeholder: 'Select knowledge base', multiSelect: false, required: true, - condition: { field: 'operation', value: ['search', 'upload_chunk', 'create_document'] }, + mode: 'basic', }, + // Knowledge Base selector — advanced mode (manual ID input) + { + id: 'manualKnowledgeBaseId', + title: 'Knowledge Base ID', + type: 'short-input', + canonicalParamId: 'knowledgeBaseId', + placeholder: 'Enter knowledge base ID', + required: true, + mode: 'advanced', + }, + + // --- Search --- { id: 'query', title: 'Search Query', @@ -57,15 +80,72 @@ export const KnowledgeBlock: BlockConfig = { placeholder: 'Add tag filters', condition: { field: 'operation', value: 'search' }, }, + + // --- List Documents --- { - id: 'documentId', + id: 'search', + title: 'Search', + type: 'short-input', + placeholder: 'Filter documents by filename', + condition: { field: 'operation', value: 'list_documents' }, + }, + { + id: 'enabledFilter', + title: 'Status Filter', + type: 'dropdown', + options: [ + { label: 'All', id: 'all' }, + { label: 'Enabled', id: 'enabled' }, + { label: 'Disabled', id: 'disabled' }, + ], + condition: { field: 'operation', value: 'list_documents' }, + }, + { + id: 'limit', + title: 'Limit', + type: 'short-input', + placeholder: 'Max items to return (default: 50)', + condition: { field: 'operation', value: ['list_documents', 'list_chunks'] }, + }, + { + id: 'offset', + title: 'Offset', + type: 'short-input', + placeholder: 'Number of items to skip (default: 0)', + condition: { field: 'operation', value: ['list_documents', 'list_chunks'] }, + }, + + // Document selector — basic mode (visual selector) + { + id: 'documentSelector', title: 'Document', type: 'document-selector', + canonicalParamId: 'documentId', placeholder: 'Select document', dependsOn: ['knowledgeBaseId'], required: true, - condition: { field: 'operation', value: 'upload_chunk' }, + mode: 'basic', + condition: { + field: 'operation', + value: ['upload_chunk', 'delete_document', 'list_chunks', 'update_chunk', 'delete_chunk'], + }, }, + // Document selector — advanced mode (manual ID input) + { + id: 'manualDocumentId', + title: 'Document ID', + type: 'short-input', + canonicalParamId: 'documentId', + placeholder: 'Enter document ID', + required: true, + mode: 'advanced', + condition: { + field: 'operation', + value: ['upload_chunk', 'delete_document', 'list_chunks', 'update_chunk', 'delete_chunk'], + }, + }, + + // --- Upload Chunk --- { id: 'content', title: 'Chunk Content', @@ -75,13 +155,15 @@ export const KnowledgeBlock: BlockConfig = { required: true, condition: { field: 'operation', value: 'upload_chunk' }, }, + + // --- Create Document --- { id: 'name', title: 'Document Name', type: 'short-input', placeholder: 'Enter document name', required: true, - condition: { field: 'operation', value: ['create_document'] }, + condition: { field: 'operation', value: 'create_document' }, }, { id: 'content', @@ -90,18 +172,75 @@ export const KnowledgeBlock: BlockConfig = { placeholder: 'Enter the document content', rows: 6, required: true, - condition: { field: 'operation', value: ['create_document'] }, + condition: { field: 'operation', value: 'create_document' }, }, - // Dynamic tag entry for Create Document { id: 'documentTags', title: 'Document Tags', type: 'document-tag-entry', condition: { field: 'operation', value: 'create_document' }, }, + + // --- Update Chunk / Delete Chunk --- + { + id: 'chunkId', + title: 'Chunk ID', + type: 'short-input', + placeholder: 'Enter chunk ID', + required: true, + condition: { field: 'operation', value: ['update_chunk', 'delete_chunk'] }, + }, + { + id: 'content', + title: 'New Content', + type: 'long-input', + placeholder: 'Enter updated chunk content', + rows: 6, + condition: { field: 'operation', value: 'update_chunk' }, + }, + { + id: 'enabled', + title: 'Enabled', + type: 'dropdown', + options: [ + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + condition: { field: 'operation', value: 'update_chunk' }, + }, + + // --- List Chunks --- + { + id: 'chunkSearch', + title: 'Search', + type: 'short-input', + placeholder: 'Filter chunks by content', + condition: { field: 'operation', value: 'list_chunks' }, + }, + { + id: 'chunkEnabledFilter', + title: 'Status Filter', + type: 'dropdown', + options: [ + { label: 'All', id: 'all' }, + { label: 'Enabled', id: 'true' }, + { label: 'Disabled', id: 'false' }, + ], + condition: { field: 'operation', value: 'list_chunks' }, + }, ], tools: { - access: ['knowledge_search', 'knowledge_upload_chunk', 'knowledge_create_document'], + access: [ + 'knowledge_search', + 'knowledge_upload_chunk', + 'knowledge_create_document', + 'knowledge_list_tags', + 'knowledge_list_documents', + 'knowledge_delete_document', + 'knowledge_list_chunks', + 'knowledge_update_chunk', + 'knowledge_delete_chunk', + ], config: { tool: (params) => { switch (params.operation) { @@ -111,25 +250,62 @@ export const KnowledgeBlock: BlockConfig = { return 'knowledge_upload_chunk' case 'create_document': return 'knowledge_create_document' + case 'list_tags': + return 'knowledge_list_tags' + case 'list_documents': + return 'knowledge_list_documents' + case 'delete_document': + return 'knowledge_delete_document' + case 'list_chunks': + return 'knowledge_list_chunks' + case 'update_chunk': + return 'knowledge_update_chunk' + case 'delete_chunk': + return 'knowledge_delete_chunk' default: return 'knowledge_search' } }, params: (params) => { - // Validate required fields for each operation - if (params.operation === 'search' && !params.knowledgeBaseId) { - throw new Error('Knowledge base ID is required for search operation') + const knowledgeBaseId = params.knowledgeBaseId ? String(params.knowledgeBaseId).trim() : '' + if (!knowledgeBaseId) { + throw new Error('Knowledge base ID is required') } - if ( - (params.operation === 'upload_chunk' || params.operation === 'create_document') && - !params.knowledgeBaseId - ) { - throw new Error( - 'Knowledge base ID is required for upload_chunk and create_document operations' - ) + params.knowledgeBaseId = knowledgeBaseId + + const docOps = [ + 'upload_chunk', + 'delete_document', + 'list_chunks', + 'update_chunk', + 'delete_chunk', + ] + if (docOps.includes(params.operation)) { + const documentId = params.documentId ? String(params.documentId).trim() : '' + if (!documentId) { + throw new Error(`Document ID is required for ${params.operation} operation`) + } + params.documentId = documentId } - if (params.operation === 'upload_chunk' && !params.documentId) { - throw new Error('Document ID is required for upload_chunk operation') + + const chunkOps = ['update_chunk', 'delete_chunk'] + if (chunkOps.includes(params.operation)) { + const chunkId = params.chunkId ? String(params.chunkId).trim() : '' + if (!chunkId) { + throw new Error(`Chunk ID is required for ${params.operation} operation`) + } + params.chunkId = chunkId + } + + // Map list_chunks sub-block fields to tool params + if (params.operation === 'list_chunks') { + if (params.chunkSearch) params.search = params.chunkSearch + if (params.chunkEnabledFilter) params.enabled = params.chunkEnabledFilter + } + + // Convert enabled dropdown string to boolean for update_chunk + if (params.operation === 'update_chunk' && typeof params.enabled === 'string') { + params.enabled = params.enabled === 'true' } return params @@ -142,12 +318,18 @@ export const KnowledgeBlock: BlockConfig = { query: { type: 'string', description: 'Search query terms' }, topK: { type: 'number', description: 'Number of results' }, documentId: { type: 'string', description: 'Document identifier' }, + chunkId: { type: 'string', description: 'Chunk identifier' }, content: { type: 'string', description: 'Content data' }, name: { type: 'string', description: 'Document name' }, - // Dynamic tag filters for search + search: { type: 'string', description: 'Search filter for documents' }, + enabledFilter: { type: 'string', description: 'Filter by enabled status' }, + enabled: { type: 'string', description: 'Enable or disable a chunk' }, + limit: { type: 'number', description: 'Max items to return' }, + offset: { type: 'number', description: 'Pagination offset' }, tagFilters: { type: 'string', description: 'Tag filter criteria' }, - // Document tags for create document (JSON string of tag objects) documentTags: { type: 'string', description: 'Document tags' }, + chunkSearch: { type: 'string', description: 'Search filter for chunks' }, + chunkEnabledFilter: { type: 'string', description: 'Filter chunks by enabled status' }, }, outputs: { results: { type: 'json', description: 'Search results' }, diff --git a/apps/sim/components/emcn/components/checkbox/checkbox.tsx b/apps/sim/components/emcn/components/checkbox/checkbox.tsx index a6939e629..c3c1f5134 100644 --- a/apps/sim/components/emcn/components/checkbox/checkbox.tsx +++ b/apps/sim/components/emcn/components/checkbox/checkbox.tsx @@ -28,7 +28,7 @@ const checkboxVariants = cva( 'border-[var(--border-1)] bg-transparent', 'focus-visible:outline-none', 'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50', - 'data-[state=checked]:border-[var(--text-primary)] data-[state=checked]:bg-[var(--text-primary)]', + 'data-[state=checked]:border-[var(--brand-tertiary-2)] data-[state=checked]:bg-[var(--brand-tertiary-2)]', ].join(' '), { variants: { diff --git a/apps/sim/components/emcn/components/date-picker/date-picker.tsx b/apps/sim/components/emcn/components/date-picker/date-picker.tsx index 3c9c4d362..67fa14d27 100644 --- a/apps/sim/components/emcn/components/date-picker/date-picker.tsx +++ b/apps/sim/components/emcn/components/date-picker/date-picker.tsx @@ -797,7 +797,7 @@ const DatePicker = React.forwardRef((props, ref side='bottom' align='start' sideOffset={4} - avoidCollisions={false} + collisionPadding={16} className={cn( 'rounded-[6px] border border-[var(--border-1)] p-0', isRangeMode ? 'w-auto' : 'w-[280px]' diff --git a/apps/sim/connectors/airtable/airtable.ts b/apps/sim/connectors/airtable/airtable.ts new file mode 100644 index 000000000..d707fef1d --- /dev/null +++ b/apps/sim/connectors/airtable/airtable.ts @@ -0,0 +1,390 @@ +import { createLogger } from '@sim/logger' +import { AirtableIcon } from '@/components/icons' +import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' + +const logger = createLogger('AirtableConnector') + +const AIRTABLE_API = 'https://api.airtable.com/v0' +const PAGE_SIZE = 100 + +/** + * Computes a SHA-256 hash of the given content. + */ +async function computeContentHash(content: string): Promise { + const data = new TextEncoder().encode(content) + const hashBuffer = await crypto.subtle.digest('SHA-256', data) + return Array.from(new Uint8Array(hashBuffer)) + .map((b) => b.toString(16).padStart(2, '0')) + .join('') +} + +/** + * Flattens a record's fields into a plain-text representation. + * Each field is rendered as "Field Name: value" on its own line. + */ +function recordToPlainText( + fields: Record, + fieldNames?: Map +): string { + const lines: string[] = [] + for (const [key, value] of Object.entries(fields)) { + if (value == null) continue + const displayName = fieldNames?.get(key) ?? key + if (Array.isArray(value)) { + // Attachments or linked records + const items = value.map((v) => { + if (typeof v === 'object' && v !== null) { + const obj = v as Record + return (obj.url as string) || (obj.name as string) || JSON.stringify(v) + } + return String(v) + }) + lines.push(`${displayName}: ${items.join(', ')}`) + } else if (typeof value === 'object') { + lines.push(`${displayName}: ${JSON.stringify(value)}`) + } else { + lines.push(`${displayName}: ${String(value)}`) + } + } + return lines.join('\n') +} + +/** + * Extracts a human-readable title from a record's fields. + * Prefers the configured title field, then falls back to common field names. + */ +function extractTitle(fields: Record, titleField?: string): string { + if (titleField && fields[titleField] != null) { + return String(fields[titleField]) + } + const candidates = ['Name', 'Title', 'name', 'title', 'Summary', 'summary'] + for (const candidate of candidates) { + if (fields[candidate] != null) { + return String(fields[candidate]) + } + } + // Fall back to first non-null string field + for (const value of Object.values(fields)) { + if (typeof value === 'string' && value.trim()) { + return value.length > 80 ? `${value.slice(0, 80)}…` : value + } + } + return 'Untitled' +} + +/** + * Parses the cursor format: "offset:" + */ +function parseCursor(cursor?: string): string | undefined { + if (!cursor) return undefined + if (cursor.startsWith('offset:')) return cursor.slice(7) + return cursor +} + +export const airtableConnector: ConnectorConfig = { + id: 'airtable', + name: 'Airtable', + description: 'Sync records from an Airtable table into your knowledge base', + version: '1.0.0', + icon: AirtableIcon, + + oauth: { + required: true, + provider: 'airtable', + requiredScopes: ['data.records:read', 'schema.bases:read'], + }, + + configFields: [ + { + id: 'baseId', + title: 'Base ID', + type: 'short-input', + placeholder: 'e.g. appXXXXXXXXXXXXXX', + required: true, + }, + { + id: 'tableIdOrName', + title: 'Table Name or ID', + type: 'short-input', + placeholder: 'e.g. Tasks or tblXXXXXXXXXXXXXX', + required: true, + }, + { + id: 'viewId', + title: 'View', + type: 'short-input', + placeholder: 'e.g. Grid view or viwXXXXXXXXXXXXXX', + required: false, + }, + { + id: 'titleField', + title: 'Title Field', + type: 'short-input', + placeholder: 'e.g. Name', + required: false, + }, + { + id: 'maxRecords', + title: 'Max Records', + type: 'short-input', + placeholder: 'e.g. 1000 (default: unlimited)', + required: false, + }, + ], + + listDocuments: async ( + accessToken: string, + sourceConfig: Record, + cursor?: string + ): Promise => { + const baseId = sourceConfig.baseId as string + const tableIdOrName = sourceConfig.tableIdOrName as string + const viewId = sourceConfig.viewId as string | undefined + const titleField = sourceConfig.titleField as string | undefined + const maxRecords = sourceConfig.maxRecords ? Number(sourceConfig.maxRecords) : 0 + + // Fetch table schema for field name mapping + const fieldNames = await fetchFieldNames(accessToken, baseId, tableIdOrName) + + const params = new URLSearchParams() + params.append('pageSize', String(PAGE_SIZE)) + if (viewId) params.append('view', viewId) + if (maxRecords > 0) params.append('maxRecords', String(maxRecords)) + + const offset = parseCursor(cursor) + if (offset) params.append('offset', offset) + + const encodedTable = encodeURIComponent(tableIdOrName) + const url = `${AIRTABLE_API}/${baseId}/${encodedTable}?${params.toString()}` + + logger.info(`Listing records from ${baseId}/${tableIdOrName}`, { + offset: offset ?? 'none', + view: viewId ?? 'default', + }) + + const response = await fetch(url, { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorText = await response.text() + logger.error('Failed to list Airtable records', { + status: response.status, + error: errorText, + }) + throw new Error(`Failed to list Airtable records: ${response.status}`) + } + + const data = (await response.json()) as { + records: AirtableRecord[] + offset?: string + } + + const records = data.records || [] + const documents: ExternalDocument[] = await Promise.all( + records.map((record) => + recordToDocument(record, baseId, tableIdOrName, titleField, fieldNames) + ) + ) + + const nextOffset = data.offset + return { + documents, + nextCursor: nextOffset ? `offset:${nextOffset}` : undefined, + hasMore: Boolean(nextOffset), + } + }, + + getDocument: async ( + accessToken: string, + sourceConfig: Record, + externalId: string + ): Promise => { + const baseId = sourceConfig.baseId as string + const tableIdOrName = sourceConfig.tableIdOrName as string + const titleField = sourceConfig.titleField as string | undefined + + const fieldNames = await fetchFieldNames(accessToken, baseId, tableIdOrName) + const encodedTable = encodeURIComponent(tableIdOrName) + const url = `${AIRTABLE_API}/${baseId}/${encodedTable}/${externalId}` + + const response = await fetch(url, { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + if (response.status === 404 || response.status === 422) return null + throw new Error(`Failed to get Airtable record: ${response.status}`) + } + + const record = (await response.json()) as AirtableRecord + return recordToDocument(record, baseId, tableIdOrName, titleField, fieldNames) + }, + + validateConfig: async ( + accessToken: string, + sourceConfig: Record + ): Promise<{ valid: boolean; error?: string }> => { + const baseId = sourceConfig.baseId as string + const tableIdOrName = sourceConfig.tableIdOrName as string + + if (!baseId || !tableIdOrName) { + return { valid: false, error: 'Base ID and table name are required' } + } + + if (baseId && !baseId.startsWith('app')) { + return { valid: false, error: 'Base ID should start with "app"' } + } + + const maxRecords = sourceConfig.maxRecords as string | undefined + if (maxRecords && (Number.isNaN(Number(maxRecords)) || Number(maxRecords) <= 0)) { + return { valid: false, error: 'Max records must be a positive number' } + } + + try { + // Verify base and table are accessible by fetching 1 record + const encodedTable = encodeURIComponent(tableIdOrName) + const url = `${AIRTABLE_API}/${baseId}/${encodedTable}?pageSize=1` + const response = await fetch(url, { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorText = await response.text() + if (response.status === 404 || response.status === 422) { + return { valid: false, error: `Table "${tableIdOrName}" not found in base "${baseId}"` } + } + if (response.status === 403) { + return { valid: false, error: 'Access denied. Check your Airtable permissions.' } + } + return { valid: false, error: `Airtable API error: ${response.status} - ${errorText}` } + } + + // If a view is specified, verify it exists + const viewId = sourceConfig.viewId as string | undefined + if (viewId) { + const viewUrl = `${AIRTABLE_API}/${baseId}/${encodedTable}?pageSize=1&view=${encodeURIComponent(viewId)}` + const viewResponse = await fetch(viewUrl, { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + if (!viewResponse.ok) { + return { valid: false, error: `View "${viewId}" not found in table "${tableIdOrName}"` } + } + } + + return { valid: true } + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to validate configuration' + return { valid: false, error: message } + } + }, + + tagDefinitions: [{ id: 'createdTime', displayName: 'Created Time', fieldType: 'date' }], + + mapTags: (metadata: Record): Record => { + const result: Record = {} + + if (typeof metadata.createdTime === 'string') { + const date = new Date(metadata.createdTime) + if (!Number.isNaN(date.getTime())) result.createdTime = date + } + + return result + }, +} + +interface AirtableRecord { + id: string + fields: Record + createdTime: string +} + +/** + * Converts an Airtable record to an ExternalDocument. + */ +async function recordToDocument( + record: AirtableRecord, + baseId: string, + tableIdOrName: string, + titleField: string | undefined, + fieldNames: Map +): Promise { + const plainText = recordToPlainText(record.fields, fieldNames) + const contentHash = await computeContentHash(plainText) + const title = extractTitle(record.fields, titleField) + + const encodedTable = encodeURIComponent(tableIdOrName) + const sourceUrl = `https://airtable.com/${baseId}/${encodedTable}/${record.id}` + + return { + externalId: record.id, + title, + content: plainText, + mimeType: 'text/plain', + sourceUrl, + contentHash, + metadata: { + createdTime: record.createdTime, + }, + } +} + +/** + * Fetches the table schema to build a field ID → field name mapping. + * Falls back to an empty map if the schema endpoint is unavailable. + */ +async function fetchFieldNames( + accessToken: string, + baseId: string, + tableIdOrName: string +): Promise> { + const fieldNames = new Map() + + try { + const url = `${AIRTABLE_API}/meta/bases/${baseId}/tables` + const response = await fetch(url, { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + logger.warn('Failed to fetch Airtable schema, using raw field keys', { + status: response.status, + }) + return fieldNames + } + + const data = (await response.json()) as { + tables: { id: string; name: string; fields: { id: string; name: string; type: string }[] }[] + } + + const table = data.tables.find((t) => t.id === tableIdOrName || t.name === tableIdOrName) + + if (table) { + for (const field of table.fields) { + fieldNames.set(field.id, field.name) + fieldNames.set(field.name, field.name) + } + } + } catch (error) { + logger.warn('Error fetching Airtable schema', { + error: error instanceof Error ? error.message : String(error), + }) + } + + return fieldNames +} diff --git a/apps/sim/connectors/airtable/index.ts b/apps/sim/connectors/airtable/index.ts new file mode 100644 index 000000000..ba99c72b3 --- /dev/null +++ b/apps/sim/connectors/airtable/index.ts @@ -0,0 +1 @@ +export { airtableConnector } from '@/connectors/airtable/airtable' diff --git a/apps/sim/connectors/confluence/confluence.ts b/apps/sim/connectors/confluence/confluence.ts new file mode 100644 index 000000000..e4ce62615 --- /dev/null +++ b/apps/sim/connectors/confluence/confluence.ts @@ -0,0 +1,611 @@ +import { createLogger } from '@sim/logger' +import { ConfluenceIcon } from '@/components/icons' +import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' +import { getConfluenceCloudId } from '@/tools/confluence/utils' + +const logger = createLogger('ConfluenceConnector') + +/** + * Strips HTML tags from content and decodes HTML entities. + */ +function htmlToPlainText(html: string): string { + let text = html.replace(/<[^>]*>/g, ' ') + text = text + .replace(/ /g, ' ') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/&/g, '&') + return text.replace(/\s+/g, ' ').trim() +} + +/** + * Computes a SHA-256 hash of the given content. + */ +async function computeContentHash(content: string): Promise { + const encoder = new TextEncoder() + const data = encoder.encode(content) + const hashBuffer = await crypto.subtle.digest('SHA-256', data) + const hashArray = Array.from(new Uint8Array(hashBuffer)) + return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('') +} + +/** + * Fetches labels for a batch of page IDs using the v2 labels endpoint. + */ +async function fetchLabelsForPages( + cloudId: string, + accessToken: string, + pageIds: string[] +): Promise> { + const labelsByPageId = new Map() + + const results = await Promise.all( + pageIds.map(async (pageId) => { + try { + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/labels` + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + logger.warn(`Failed to fetch labels for page ${pageId}`, { status: response.status }) + return { pageId, labels: [] as string[] } + } + + const data = await response.json() + const labels = (data.results || []).map( + (label: Record) => label.name as string + ) + return { pageId, labels } + } catch (error) { + logger.warn(`Error fetching labels for page ${pageId}`, { + error: error instanceof Error ? error.message : String(error), + }) + return { pageId, labels: [] as string[] } + } + }) + ) + + for (const { pageId, labels } of results) { + labelsByPageId.set(pageId, labels) + } + + return labelsByPageId +} + +/** + * Converts a v1 CQL search result item to an ExternalDocument. + */ +async function cqlResultToDocument( + item: Record, + domain: string +): Promise { + const body = item.body as Record> | undefined + const rawContent = body?.storage?.value || '' + const plainText = htmlToPlainText(rawContent) + const contentHash = await computeContentHash(plainText) + + const version = item.version as Record | undefined + const links = item._links as Record | undefined + const metadata = item.metadata as Record | undefined + const labelsWrapper = metadata?.labels as Record | undefined + const labelResults = (labelsWrapper?.results || []) as Record[] + const labels = labelResults.map((l) => l.name as string) + + return { + externalId: String(item.id), + title: (item.title as string) || 'Untitled', + content: plainText, + mimeType: 'text/plain', + sourceUrl: links?.webui ? `https://${domain}/wiki${links.webui}` : undefined, + contentHash, + metadata: { + spaceId: (item.space as Record)?.key, + status: item.status, + version: version?.number, + labels, + lastModified: version?.when, + }, + } +} + +export const confluenceConnector: ConnectorConfig = { + id: 'confluence', + name: 'Confluence', + description: 'Sync pages from a Confluence space into your knowledge base', + version: '1.1.0', + icon: ConfluenceIcon, + + oauth: { + required: true, + provider: 'confluence', + requiredScopes: ['read:confluence-content.all', 'read:page:confluence', 'offline_access'], + }, + + configFields: [ + { + id: 'domain', + title: 'Confluence Domain', + type: 'short-input', + placeholder: 'yoursite.atlassian.net', + required: true, + }, + { + id: 'spaceKey', + title: 'Space Key', + type: 'short-input', + placeholder: 'e.g. ENG, PRODUCT', + required: true, + }, + { + id: 'contentType', + title: 'Content Type', + type: 'dropdown', + required: false, + options: [ + { label: 'Pages only', id: 'page' }, + { label: 'Blog posts only', id: 'blogpost' }, + { label: 'All content', id: 'all' }, + ], + }, + { + id: 'labelFilter', + title: 'Filter by Label', + type: 'short-input', + required: false, + placeholder: 'e.g. published, engineering', + }, + { + id: 'maxPages', + title: 'Max Pages', + type: 'short-input', + required: false, + placeholder: 'e.g. 500 (default: unlimited)', + }, + ], + + listDocuments: async ( + accessToken: string, + sourceConfig: Record, + cursor?: string + ): Promise => { + const domain = sourceConfig.domain as string + const spaceKey = sourceConfig.spaceKey as string + const contentType = (sourceConfig.contentType as string) || 'page' + const labelFilter = (sourceConfig.labelFilter as string) || '' + const maxPages = sourceConfig.maxPages ? Number(sourceConfig.maxPages) : 0 + + const cloudId = await getConfluenceCloudId(domain, accessToken) + + // If label filtering is enabled, use CQL search via v1 API + if (labelFilter.trim()) { + return listDocumentsViaCql( + cloudId, + accessToken, + domain, + spaceKey, + contentType, + labelFilter, + maxPages, + cursor + ) + } + + // Otherwise use v2 API (default path) + const spaceId = await resolveSpaceId(cloudId, accessToken, spaceKey) + + if (contentType === 'all') { + return listAllContentTypes(cloudId, accessToken, domain, spaceId, spaceKey, maxPages, cursor) + } + + return listDocumentsV2( + cloudId, + accessToken, + domain, + spaceId, + spaceKey, + contentType, + maxPages, + cursor + ) + }, + + getDocument: async ( + accessToken: string, + sourceConfig: Record, + externalId: string + ): Promise => { + const domain = sourceConfig.domain as string + const cloudId = await getConfluenceCloudId(domain, accessToken) + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${externalId}?body-format=storage` + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + if (response.status === 404) return null + throw new Error(`Failed to get Confluence page: ${response.status}`) + } + + const page = await response.json() + const rawContent = page.body?.storage?.value || '' + const plainText = htmlToPlainText(rawContent) + const contentHash = await computeContentHash(plainText) + + // Fetch labels for this page + const labelMap = await fetchLabelsForPages(cloudId, accessToken, [String(page.id)]) + const labels = labelMap.get(String(page.id)) ?? [] + + return { + externalId: String(page.id), + title: page.title || 'Untitled', + content: plainText, + mimeType: 'text/plain', + sourceUrl: page._links?.webui ? `https://${domain}/wiki${page._links.webui}` : undefined, + contentHash, + metadata: { + spaceId: page.spaceId, + status: page.status, + version: page.version?.number, + labels, + lastModified: page.version?.createdAt, + }, + } + }, + + validateConfig: async ( + accessToken: string, + sourceConfig: Record + ): Promise<{ valid: boolean; error?: string }> => { + const domain = sourceConfig.domain as string + const spaceKey = sourceConfig.spaceKey as string + + if (!domain || !spaceKey) { + return { valid: false, error: 'Domain and space key are required' } + } + + const maxPages = sourceConfig.maxPages as string | undefined + if (maxPages && (Number.isNaN(Number(maxPages)) || Number(maxPages) <= 0)) { + return { valid: false, error: 'Max pages must be a positive number' } + } + + try { + const cloudId = await getConfluenceCloudId(domain, accessToken) + await resolveSpaceId(cloudId, accessToken, spaceKey) + return { valid: true } + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to validate configuration' + return { valid: false, error: message } + } + }, + + tagDefinitions: [ + { id: 'labels', displayName: 'Labels', fieldType: 'text' }, + { id: 'version', displayName: 'Version', fieldType: 'number' }, + { id: 'lastModified', displayName: 'Last Modified', fieldType: 'date' }, + ], + + mapTags: (metadata: Record): Record => { + const result: Record = {} + + const labels = Array.isArray(metadata.labels) ? (metadata.labels as string[]) : [] + if (labels.length > 0) result.labels = labels.join(', ') + + if (metadata.version != null) { + const num = Number(metadata.version) + if (!Number.isNaN(num)) result.version = num + } + + if (typeof metadata.lastModified === 'string') { + const date = new Date(metadata.lastModified) + if (!Number.isNaN(date.getTime())) result.lastModified = date + } + + return result + }, +} + +/** + * Lists documents using the v2 API for a single content type (pages or blogposts). + */ +async function listDocumentsV2( + cloudId: string, + accessToken: string, + domain: string, + spaceId: string, + spaceKey: string, + contentType: string, + maxPages: number, + cursor?: string +): Promise { + const queryParams = new URLSearchParams() + queryParams.append('limit', '50') + queryParams.append('body-format', 'storage') + if (cursor) { + queryParams.append('cursor', cursor) + } + + const endpoint = contentType === 'blogpost' ? 'blogposts' : 'pages' + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}/${endpoint}?${queryParams.toString()}` + + logger.info(`Listing ${endpoint} in space ${spaceKey} (ID: ${spaceId})`) + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorText = await response.text() + logger.error(`Failed to list Confluence ${endpoint}`, { + status: response.status, + error: errorText, + }) + throw new Error(`Failed to list Confluence ${endpoint}: ${response.status}`) + } + + const data = await response.json() + const results = data.results || [] + + // Fetch labels for all pages in this batch + const pageIds = results.map((page: Record) => String(page.id)) + const labelsByPageId = await fetchLabelsForPages(cloudId, accessToken, pageIds) + + const documents: ExternalDocument[] = await Promise.all( + results.map(async (page: Record) => { + const rawContent = (page.body as Record>)?.storage?.value || '' + const plainText = htmlToPlainText(rawContent) + const contentHash = await computeContentHash(plainText) + const pageId = String(page.id) + + return { + externalId: pageId, + title: (page.title as string) || 'Untitled', + content: plainText, + mimeType: 'text/plain', + sourceUrl: (page._links as Record)?.webui + ? `https://${domain}/wiki${(page._links as Record).webui}` + : undefined, + contentHash, + metadata: { + spaceId: page.spaceId, + status: page.status, + version: (page.version as Record)?.number, + labels: labelsByPageId.get(pageId) ?? [], + lastModified: (page.version as Record)?.createdAt, + }, + } + }) + ) + + // Extract next cursor from _links.next + let nextCursor: string | undefined + const nextLink = (data._links as Record)?.next + if (nextLink) { + try { + nextCursor = new URL(nextLink, 'https://placeholder').searchParams.get('cursor') || undefined + } catch { + // Ignore malformed URLs + } + } + + // Enforce maxPages limit + if (maxPages > 0 && !cursor) { + // On subsequent pages, the sync engine tracks total count + // We signal stop by clearing hasMore when we'd exceed maxPages + } + + return { + documents, + nextCursor, + hasMore: Boolean(nextCursor), + } +} + +/** + * Lists both pages and blogposts using a compound cursor that tracks + * pagination state for each content type independently. + */ +async function listAllContentTypes( + cloudId: string, + accessToken: string, + domain: string, + spaceId: string, + spaceKey: string, + maxPages: number, + cursor?: string +): Promise { + let pageCursor: string | undefined + let blogCursor: string | undefined + let pagesDone = false + let blogsDone = false + + if (cursor) { + try { + const parsed = JSON.parse(cursor) + pageCursor = parsed.page + blogCursor = parsed.blog + pagesDone = parsed.pagesDone === true + blogsDone = parsed.blogsDone === true + } catch { + pageCursor = cursor + } + } + + const results: ExternalDocumentList = { documents: [], hasMore: false } + + if (!pagesDone) { + const pagesResult = await listDocumentsV2( + cloudId, + accessToken, + domain, + spaceId, + spaceKey, + 'page', + maxPages, + pageCursor + ) + results.documents.push(...pagesResult.documents) + pageCursor = pagesResult.nextCursor + pagesDone = !pagesResult.hasMore + } + + if (!blogsDone) { + const blogResult = await listDocumentsV2( + cloudId, + accessToken, + domain, + spaceId, + spaceKey, + 'blogpost', + maxPages, + blogCursor + ) + results.documents.push(...blogResult.documents) + blogCursor = blogResult.nextCursor + blogsDone = !blogResult.hasMore + } + + results.hasMore = !pagesDone || !blogsDone + + if (results.hasMore) { + results.nextCursor = JSON.stringify({ + page: pageCursor, + blog: blogCursor, + pagesDone, + blogsDone, + }) + } + + return results +} + +/** + * Lists documents using CQL search via the v1 API (used when label filtering is enabled). + */ +async function listDocumentsViaCql( + cloudId: string, + accessToken: string, + domain: string, + spaceKey: string, + contentType: string, + labelFilter: string, + maxPages: number, + cursor?: string +): Promise { + const labels = labelFilter + .split(',') + .map((l) => l.trim()) + .filter(Boolean) + + // Build CQL query + let cql = `space="${spaceKey}"` + + if (contentType === 'blogpost') { + cql += ' AND type="blogpost"' + } else if (contentType === 'page' || !contentType) { + cql += ' AND type="page"' + } + // contentType === 'all' — no type filter + + if (labels.length === 1) { + cql += ` AND label="${labels[0]}"` + } else if (labels.length > 1) { + const labelList = labels.map((l) => `"${l}"`).join(',') + cql += ` AND label in (${labelList})` + } + + const limit = maxPages > 0 ? Math.min(maxPages, 50) : 50 + const start = cursor ? Number(cursor) : 0 + + const queryParams = new URLSearchParams() + queryParams.append('cql', cql) + queryParams.append('limit', String(limit)) + queryParams.append('start', String(start)) + queryParams.append('expand', 'body.storage,version,metadata.labels') + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/rest/api/content/search?${queryParams.toString()}` + + logger.info(`Searching Confluence via CQL: ${cql}`, { start, limit }) + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorText = await response.text() + logger.error('Failed to search Confluence via CQL', { + status: response.status, + error: errorText, + }) + throw new Error(`Failed to search Confluence via CQL: ${response.status}`) + } + + const data = await response.json() + const results = data.results || [] + + const documents: ExternalDocument[] = await Promise.all( + results.map((item: Record) => cqlResultToDocument(item, domain)) + ) + + const totalSize = (data.totalSize as number) ?? 0 + const nextStart = start + results.length + const hasMore = nextStart < totalSize && (maxPages <= 0 || nextStart < maxPages) + + return { + documents, + nextCursor: hasMore ? String(nextStart) : undefined, + hasMore, + } +} + +/** + * Resolves a Confluence space key to its numeric space ID. + */ +async function resolveSpaceId( + cloudId: string, + accessToken: string, + spaceKey: string +): Promise { + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces?keys=${encodeURIComponent(spaceKey)}&limit=1` + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + throw new Error(`Failed to resolve space key "${spaceKey}": ${response.status}`) + } + + const data = await response.json() + const results = data.results || [] + + if (results.length === 0) { + throw new Error(`Space "${spaceKey}" not found`) + } + + return String(results[0].id) +} diff --git a/apps/sim/connectors/confluence/index.ts b/apps/sim/connectors/confluence/index.ts new file mode 100644 index 000000000..c32e32988 --- /dev/null +++ b/apps/sim/connectors/confluence/index.ts @@ -0,0 +1 @@ +export { confluenceConnector } from '@/connectors/confluence/confluence' diff --git a/apps/sim/connectors/github/github.ts b/apps/sim/connectors/github/github.ts new file mode 100644 index 000000000..6f122a61b --- /dev/null +++ b/apps/sim/connectors/github/github.ts @@ -0,0 +1,409 @@ +import { createLogger } from '@sim/logger' +import { GithubIcon } from '@/components/icons' +import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' + +const logger = createLogger('GitHubConnector') + +const GITHUB_API_URL = 'https://api.github.com' +const BATCH_SIZE = 30 + +/** + * Computes a SHA-256 hash of the given content. + */ +async function computeContentHash(content: string): Promise { + const data = new TextEncoder().encode(content) + const hashBuffer = await crypto.subtle.digest('SHA-256', data) + return Array.from(new Uint8Array(hashBuffer)) + .map((b) => b.toString(16).padStart(2, '0')) + .join('') +} + +/** + * Parses the repository string into owner and repo. + */ +function parseRepo(repository: string): { owner: string; repo: string } { + const cleaned = repository.replace(/^https?:\/\/github\.com\//, '').replace(/\.git$/, '') + const parts = cleaned.split('/') + if (parts.length < 2 || !parts[0] || !parts[1]) { + throw new Error(`Invalid repository format: "${repository}". Use "owner/repo".`) + } + return { owner: parts[0], repo: parts[1] } +} + +/** + * File extension filter set from user config. Returns null if no filter (accept all). + */ +function parseExtensions(extensions: string): Set | null { + const trimmed = extensions.trim() + if (!trimmed) return null + const exts = trimmed + .split(',') + .map((e) => e.trim().toLowerCase()) + .filter(Boolean) + .map((e) => (e.startsWith('.') ? e : `.${e}`)) + return exts.length > 0 ? new Set(exts) : null +} + +/** + * Checks whether a file path matches the extension filter. + */ +function matchesExtension(filePath: string, extSet: Set | null): boolean { + if (!extSet) return true + const lastDot = filePath.lastIndexOf('.') + if (lastDot === -1) return false + return extSet.has(filePath.slice(lastDot).toLowerCase()) +} + +interface TreeItem { + path: string + mode: string + type: string + sha: string + size?: number +} + +/** + * Fetches the full recursive tree for a branch. + */ +async function fetchTree( + accessToken: string, + owner: string, + repo: string, + branch: string +): Promise { + const url = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/trees/${encodeURIComponent(branch)}?recursive=1` + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/vnd.github+json', + Authorization: `Bearer ${accessToken}`, + 'X-GitHub-Api-Version': '2022-11-28', + }, + }) + + if (!response.ok) { + const errorText = await response.text() + logger.error('Failed to fetch GitHub tree', { status: response.status, error: errorText }) + throw new Error(`Failed to fetch repository tree: ${response.status}`) + } + + const data = await response.json() + + if (data.truncated) { + logger.warn('GitHub tree was truncated — some files may be missing', { owner, repo, branch }) + } + + return (data.tree || []).filter((item: TreeItem) => item.type === 'blob') +} + +/** + * Fetches file content via the Blobs API and decodes base64. + */ +async function fetchBlobContent( + accessToken: string, + owner: string, + repo: string, + sha: string +): Promise { + const url = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/blobs/${sha}` + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/vnd.github+json', + Authorization: `Bearer ${accessToken}`, + 'X-GitHub-Api-Version': '2022-11-28', + }, + }) + + if (!response.ok) { + throw new Error(`Failed to fetch blob ${sha}: ${response.status}`) + } + + const data = await response.json() + + if (data.encoding === 'base64') { + return atob(data.content.replace(/\n/g, '')) + } + + return data.content || '' +} + +/** + * Converts a tree item to an ExternalDocument by fetching its content. + */ +async function treeItemToDocument( + accessToken: string, + owner: string, + repo: string, + branch: string, + item: TreeItem +): Promise { + const content = await fetchBlobContent(accessToken, owner, repo, item.sha) + const contentHash = await computeContentHash(content) + + return { + externalId: item.path, + title: item.path.split('/').pop() || item.path, + content, + mimeType: 'text/plain', + sourceUrl: `https://github.com/${owner}/${repo}/blob/${branch}/${item.path}`, + contentHash, + metadata: { + path: item.path, + sha: item.sha, + size: item.size, + branch, + repository: `${owner}/${repo}`, + }, + } +} + +export const githubConnector: ConnectorConfig = { + id: 'github', + name: 'GitHub', + description: 'Sync files from a GitHub repository into your knowledge base', + version: '1.0.0', + icon: GithubIcon, + + oauth: { + required: true, + provider: 'github', + requiredScopes: ['repo'], + }, + + configFields: [ + { + id: 'repository', + title: 'Repository', + type: 'short-input', + placeholder: 'owner/repo', + required: true, + }, + { + id: 'branch', + title: 'Branch', + type: 'short-input', + placeholder: 'main (default)', + required: false, + }, + { + id: 'pathPrefix', + title: 'Path Filter', + type: 'short-input', + placeholder: 'e.g. docs/, src/components/', + required: false, + }, + { + id: 'extensions', + title: 'File Extensions', + type: 'short-input', + placeholder: 'e.g. .md, .txt, .mdx', + required: false, + }, + { + id: 'maxFiles', + title: 'Max Files', + type: 'short-input', + required: false, + placeholder: 'e.g. 500 (default: unlimited)', + }, + ], + + listDocuments: async ( + accessToken: string, + sourceConfig: Record, + cursor?: string + ): Promise => { + const { owner, repo } = parseRepo(sourceConfig.repository as string) + const branch = ((sourceConfig.branch as string) || 'main').trim() + const pathPrefix = ((sourceConfig.pathPrefix as string) || '').trim() + const extSet = parseExtensions((sourceConfig.extensions as string) || '') + const maxFiles = sourceConfig.maxFiles ? Number(sourceConfig.maxFiles) : 0 + + const tree = await fetchTree(accessToken, owner, repo, branch) + + // Filter by path prefix and extensions + const filtered = tree.filter((item) => { + if (pathPrefix && !item.path.startsWith(pathPrefix)) return false + if (!matchesExtension(item.path, extSet)) return false + return true + }) + + // Apply max files limit + const capped = maxFiles > 0 ? filtered.slice(0, maxFiles) : filtered + + // Paginate using offset cursor + const offset = cursor ? Number(cursor) : 0 + const batch = capped.slice(offset, offset + BATCH_SIZE) + + logger.info('Listing GitHub files', { + owner, + repo, + branch, + totalFiltered: capped.length, + offset, + batchSize: batch.length, + }) + + const documents: ExternalDocument[] = [] + for (const item of batch) { + try { + const doc = await treeItemToDocument(accessToken, owner, repo, branch, item) + documents.push(doc) + } catch (error) { + logger.warn(`Failed to fetch content for ${item.path}`, { + error: error instanceof Error ? error.message : String(error), + }) + } + } + + const nextOffset = offset + BATCH_SIZE + const hasMore = nextOffset < capped.length + + return { + documents, + nextCursor: hasMore ? String(nextOffset) : undefined, + hasMore, + } + }, + + getDocument: async ( + accessToken: string, + sourceConfig: Record, + externalId: string + ): Promise => { + const { owner, repo } = parseRepo(sourceConfig.repository as string) + const branch = ((sourceConfig.branch as string) || 'main').trim() + + // externalId is the file path + const path = externalId + + try { + const url = `${GITHUB_API_URL}/repos/${owner}/${repo}/contents/${encodeURIComponent(path)}?ref=${encodeURIComponent(branch)}` + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/vnd.github+json', + Authorization: `Bearer ${accessToken}`, + 'X-GitHub-Api-Version': '2022-11-28', + }, + }) + + if (!response.ok) { + if (response.status === 404) return null + throw new Error(`Failed to fetch file ${path}: ${response.status}`) + } + + const data = await response.json() + const content = + data.encoding === 'base64' + ? atob((data.content as string).replace(/\n/g, '')) + : (data.content as string) || '' + const contentHash = await computeContentHash(content) + + return { + externalId, + title: path.split('/').pop() || path, + content, + mimeType: 'text/plain', + sourceUrl: `https://github.com/${owner}/${repo}/blob/${branch}/${path}`, + contentHash, + metadata: { + path, + sha: data.sha as string, + size: data.size as number, + branch, + repository: `${owner}/${repo}`, + }, + } + } catch (error) { + logger.warn(`Failed to fetch GitHub document ${externalId}`, { + error: error instanceof Error ? error.message : String(error), + }) + return null + } + }, + + validateConfig: async ( + accessToken: string, + sourceConfig: Record + ): Promise<{ valid: boolean; error?: string }> => { + const repository = (sourceConfig.repository as string)?.trim() + if (!repository) { + return { valid: false, error: 'Repository is required' } + } + + let owner: string + let repo: string + try { + const parsed = parseRepo(repository) + owner = parsed.owner + repo = parsed.repo + } catch (error) { + return { + valid: false, + error: error instanceof Error ? error.message : 'Invalid repository format', + } + } + + const maxFiles = sourceConfig.maxFiles as string | undefined + if (maxFiles && (Number.isNaN(Number(maxFiles)) || Number(maxFiles) <= 0)) { + return { valid: false, error: 'Max files must be a positive number' } + } + + const branch = ((sourceConfig.branch as string) || 'main').trim() + + try { + // Verify repo and branch are accessible + const url = `${GITHUB_API_URL}/repos/${owner}/${repo}/branches/${encodeURIComponent(branch)}` + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/vnd.github+json', + Authorization: `Bearer ${accessToken}`, + 'X-GitHub-Api-Version': '2022-11-28', + }, + }) + + if (response.status === 404) { + return { + valid: false, + error: `Repository "${owner}/${repo}" or branch "${branch}" not found`, + } + } + + if (!response.ok) { + return { valid: false, error: `Cannot access repository: ${response.status}` } + } + + return { valid: true } + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to validate configuration' + return { valid: false, error: message } + } + }, + + tagDefinitions: [ + { id: 'path', displayName: 'File Path', fieldType: 'text' }, + { id: 'repository', displayName: 'Repository', fieldType: 'text' }, + { id: 'branch', displayName: 'Branch', fieldType: 'text' }, + { id: 'size', displayName: 'File Size', fieldType: 'number' }, + ], + + mapTags: (metadata: Record): Record => { + const result: Record = {} + + if (typeof metadata.path === 'string') result.path = metadata.path + if (typeof metadata.repository === 'string') result.repository = metadata.repository + if (typeof metadata.branch === 'string') result.branch = metadata.branch + + if (metadata.size != null) { + const num = Number(metadata.size) + if (!Number.isNaN(num)) result.size = num + } + + return result + }, +} diff --git a/apps/sim/connectors/github/index.ts b/apps/sim/connectors/github/index.ts new file mode 100644 index 000000000..cba1b440c --- /dev/null +++ b/apps/sim/connectors/github/index.ts @@ -0,0 +1 @@ +export { githubConnector } from '@/connectors/github/github' diff --git a/apps/sim/connectors/google-drive/google-drive.ts b/apps/sim/connectors/google-drive/google-drive.ts new file mode 100644 index 000000000..f339e0594 --- /dev/null +++ b/apps/sim/connectors/google-drive/google-drive.ts @@ -0,0 +1,424 @@ +import { createLogger } from '@sim/logger' +import { GoogleDriveIcon } from '@/components/icons' +import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' + +const logger = createLogger('GoogleDriveConnector') + +const GOOGLE_WORKSPACE_MIME_TYPES: Record = { + 'application/vnd.google-apps.document': 'text/plain', + 'application/vnd.google-apps.spreadsheet': 'text/csv', + 'application/vnd.google-apps.presentation': 'text/plain', +} + +const SUPPORTED_TEXT_MIME_TYPES = [ + 'text/plain', + 'text/csv', + 'text/html', + 'text/markdown', + 'application/json', + 'application/xml', +] + +const MAX_EXPORT_SIZE = 10 * 1024 * 1024 // 10 MB (Google export limit) + +async function computeContentHash(content: string): Promise { + const data = new TextEncoder().encode(content) + const hashBuffer = await crypto.subtle.digest('SHA-256', data) + return Array.from(new Uint8Array(hashBuffer)) + .map((b) => b.toString(16).padStart(2, '0')) + .join('') +} + +function isGoogleWorkspaceFile(mimeType: string): boolean { + return mimeType in GOOGLE_WORKSPACE_MIME_TYPES +} + +function isSupportedTextFile(mimeType: string): boolean { + return SUPPORTED_TEXT_MIME_TYPES.some((t) => mimeType.startsWith(t)) +} + +function htmlToPlainText(html: string): string { + let text = html.replace(/<[^>]*>/g, ' ') + text = text + .replace(/ /g, ' ') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/&/g, '&') + return text.replace(/\s+/g, ' ').trim() +} + +async function exportGoogleWorkspaceFile( + accessToken: string, + fileId: string, + sourceMimeType: string +): Promise { + const exportMimeType = GOOGLE_WORKSPACE_MIME_TYPES[sourceMimeType] + if (!exportMimeType) { + throw new Error(`Unsupported Google Workspace MIME type: ${sourceMimeType}`) + } + + const url = `https://www.googleapis.com/drive/v3/files/${fileId}/export?mimeType=${encodeURIComponent(exportMimeType)}` + + const response = await fetch(url, { + method: 'GET', + headers: { Authorization: `Bearer ${accessToken}` }, + }) + + if (!response.ok) { + throw new Error(`Failed to export file ${fileId}: ${response.status}`) + } + + return response.text() +} + +async function downloadTextFile(accessToken: string, fileId: string): Promise { + const url = `https://www.googleapis.com/drive/v3/files/${fileId}?alt=media` + + const response = await fetch(url, { + method: 'GET', + headers: { Authorization: `Bearer ${accessToken}` }, + }) + + if (!response.ok) { + throw new Error(`Failed to download file ${fileId}: ${response.status}`) + } + + const text = await response.text() + if (text.length > MAX_EXPORT_SIZE) { + return text.slice(0, MAX_EXPORT_SIZE) + } + return text +} + +async function fetchFileContent( + accessToken: string, + fileId: string, + mimeType: string +): Promise { + if (isGoogleWorkspaceFile(mimeType)) { + return exportGoogleWorkspaceFile(accessToken, fileId, mimeType) + } + if (mimeType === 'text/html') { + const html = await downloadTextFile(accessToken, fileId) + return htmlToPlainText(html) + } + if (isSupportedTextFile(mimeType)) { + return downloadTextFile(accessToken, fileId) + } + + throw new Error(`Unsupported MIME type for content extraction: ${mimeType}`) +} + +interface DriveFile { + id: string + name: string + mimeType: string + modifiedTime?: string + createdTime?: string + webViewLink?: string + parents?: string[] + owners?: { displayName?: string; emailAddress?: string }[] + size?: string + starred?: boolean + trashed?: boolean +} + +function buildQuery(sourceConfig: Record): string { + const parts: string[] = ['trashed = false'] + + const folderId = sourceConfig.folderId as string | undefined + if (folderId?.trim()) { + parts.push(`'${folderId.trim()}' in parents`) + } + + const fileType = (sourceConfig.fileType as string) || 'all' + switch (fileType) { + case 'documents': + parts.push("mimeType = 'application/vnd.google-apps.document'") + break + case 'spreadsheets': + parts.push("mimeType = 'application/vnd.google-apps.spreadsheet'") + break + case 'presentations': + parts.push("mimeType = 'application/vnd.google-apps.presentation'") + break + case 'text': + parts.push(`(${SUPPORTED_TEXT_MIME_TYPES.map((t) => `mimeType = '${t}'`).join(' or ')})`) + break + default: { + // Include Google Workspace files + plain text files, exclude folders + const allMimeTypes = [ + ...Object.keys(GOOGLE_WORKSPACE_MIME_TYPES), + ...SUPPORTED_TEXT_MIME_TYPES, + ] + parts.push(`(${allMimeTypes.map((t) => `mimeType = '${t}'`).join(' or ')})`) + break + } + } + + return parts.join(' and ') +} + +async function fileToDocument( + accessToken: string, + file: DriveFile +): Promise { + try { + const content = await fetchFileContent(accessToken, file.id, file.mimeType) + if (!content.trim()) { + logger.info(`Skipping empty file: ${file.name} (${file.id})`) + return null + } + + const contentHash = await computeContentHash(content) + + return { + externalId: file.id, + title: file.name || 'Untitled', + content, + mimeType: 'text/plain', + sourceUrl: file.webViewLink || `https://drive.google.com/file/d/${file.id}/view`, + contentHash, + metadata: { + originalMimeType: file.mimeType, + modifiedTime: file.modifiedTime, + createdTime: file.createdTime, + owners: file.owners?.map((o) => o.displayName || o.emailAddress).filter(Boolean), + starred: file.starred, + fileSize: file.size ? Number(file.size) : undefined, + }, + } + } catch (error) { + logger.warn(`Failed to extract content from file: ${file.name} (${file.id})`, { + error: error instanceof Error ? error.message : String(error), + }) + return null + } +} + +export const googleDriveConnector: ConnectorConfig = { + id: 'google_drive', + name: 'Google Drive', + description: 'Sync documents from Google Drive into your knowledge base', + version: '1.0.0', + icon: GoogleDriveIcon, + + oauth: { + required: true, + provider: 'google-drive', + requiredScopes: ['https://www.googleapis.com/auth/drive.readonly'], + }, + + configFields: [ + { + id: 'folderId', + title: 'Folder ID', + type: 'short-input', + placeholder: 'e.g. 1aBcDeFgHiJkLmNoPqRsTuVwXyZ (optional)', + required: false, + }, + { + id: 'fileType', + title: 'File Type', + type: 'dropdown', + required: false, + options: [ + { label: 'All supported files', id: 'all' }, + { label: 'Google Docs only', id: 'documents' }, + { label: 'Google Sheets only', id: 'spreadsheets' }, + { label: 'Google Slides only', id: 'presentations' }, + { label: 'Plain text files only', id: 'text' }, + ], + }, + { + id: 'maxFiles', + title: 'Max Files', + type: 'short-input', + required: false, + placeholder: 'e.g. 500 (default: unlimited)', + }, + ], + + listDocuments: async ( + accessToken: string, + sourceConfig: Record, + cursor?: string + ): Promise => { + const query = buildQuery(sourceConfig) + const pageSize = 100 + + const queryParams = new URLSearchParams({ + q: query, + pageSize: String(pageSize), + fields: + 'nextPageToken,files(id,name,mimeType,modifiedTime,createdTime,webViewLink,parents,owners,size,starred)', + supportsAllDrives: 'true', + includeItemsFromAllDrives: 'true', + }) + + if (cursor) { + queryParams.set('pageToken', cursor) + } + + const url = `https://www.googleapis.com/drive/v3/files?${queryParams.toString()}` + + logger.info('Listing Google Drive files', { query, cursor: cursor ?? 'initial' }) + + const response = await fetch(url, { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + }, + }) + + if (!response.ok) { + const errorText = await response.text() + logger.error('Failed to list Google Drive files', { + status: response.status, + error: errorText, + }) + throw new Error(`Failed to list Google Drive files: ${response.status}`) + } + + const data = await response.json() + const files = (data.files || []) as DriveFile[] + + const documentResults = await Promise.all( + files.map((file) => fileToDocument(accessToken, file)) + ) + const documents = documentResults.filter(Boolean) as ExternalDocument[] + + const nextPageToken = data.nextPageToken as string | undefined + const hasMore = Boolean(nextPageToken) + + return { + documents, + nextCursor: nextPageToken, + hasMore, + } + }, + + getDocument: async ( + accessToken: string, + sourceConfig: Record, + externalId: string + ): Promise => { + const fields = + 'id,name,mimeType,modifiedTime,createdTime,webViewLink,parents,owners,size,starred,trashed' + const url = `https://www.googleapis.com/drive/v3/files/${externalId}?fields=${encodeURIComponent(fields)}&supportsAllDrives=true` + + const response = await fetch(url, { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + }, + }) + + if (!response.ok) { + if (response.status === 404) return null + throw new Error(`Failed to get Google Drive file: ${response.status}`) + } + + const file = (await response.json()) as DriveFile + + if (file.trashed) return null + + return fileToDocument(accessToken, file) + }, + + validateConfig: async ( + accessToken: string, + sourceConfig: Record + ): Promise<{ valid: boolean; error?: string }> => { + const folderId = sourceConfig.folderId as string | undefined + const maxFiles = sourceConfig.maxFiles as string | undefined + + if (maxFiles && (Number.isNaN(Number(maxFiles)) || Number(maxFiles) <= 0)) { + return { valid: false, error: 'Max files must be a positive number' } + } + + // Verify access to Drive API + try { + if (folderId?.trim()) { + // Verify the folder exists and is accessible + const url = `https://www.googleapis.com/drive/v3/files/${folderId.trim()}?fields=id,name,mimeType&supportsAllDrives=true` + const response = await fetch(url, { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + }, + }) + + if (!response.ok) { + if (response.status === 404) { + return { valid: false, error: 'Folder not found. Check the folder ID and permissions.' } + } + return { valid: false, error: `Failed to access folder: ${response.status}` } + } + + const folder = await response.json() + if (folder.mimeType !== 'application/vnd.google-apps.folder') { + return { valid: false, error: 'The provided ID is not a folder' } + } + } else { + // Verify basic Drive access by listing one file + const url = 'https://www.googleapis.com/drive/v3/files?pageSize=1&fields=files(id)' + const response = await fetch(url, { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + }, + }) + + if (!response.ok) { + return { valid: false, error: `Failed to access Google Drive: ${response.status}` } + } + } + + return { valid: true } + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to validate configuration' + return { valid: false, error: message } + } + }, + + tagDefinitions: [ + { id: 'owners', displayName: 'Owner', fieldType: 'text' }, + { id: 'fileType', displayName: 'File Type', fieldType: 'text' }, + { id: 'lastModified', displayName: 'Last Modified', fieldType: 'date' }, + { id: 'starred', displayName: 'Starred', fieldType: 'boolean' }, + ], + + mapTags: (metadata: Record): Record => { + const result: Record = {} + + const owners = Array.isArray(metadata.owners) ? (metadata.owners as string[]) : [] + if (owners.length > 0) result.owners = owners.join(', ') + + if (typeof metadata.originalMimeType === 'string') { + const mimeType = metadata.originalMimeType + if (mimeType.includes('document')) result.fileType = 'Google Doc' + else if (mimeType.includes('spreadsheet')) result.fileType = 'Google Sheet' + else if (mimeType.includes('presentation')) result.fileType = 'Google Slides' + else if (mimeType.startsWith('text/')) result.fileType = 'Text File' + else result.fileType = mimeType + } + + if (typeof metadata.modifiedTime === 'string') { + const date = new Date(metadata.modifiedTime) + if (!Number.isNaN(date.getTime())) result.lastModified = date + } + + if (typeof metadata.starred === 'boolean') { + result.starred = metadata.starred + } + + return result + }, +} diff --git a/apps/sim/connectors/google-drive/index.ts b/apps/sim/connectors/google-drive/index.ts new file mode 100644 index 000000000..f7b8fe770 --- /dev/null +++ b/apps/sim/connectors/google-drive/index.ts @@ -0,0 +1 @@ +export { googleDriveConnector } from '@/connectors/google-drive/google-drive' diff --git a/apps/sim/connectors/icons.ts b/apps/sim/connectors/icons.ts new file mode 100644 index 000000000..4ffdd2776 --- /dev/null +++ b/apps/sim/connectors/icons.ts @@ -0,0 +1,20 @@ +import { ConfluenceIcon, GithubIcon, LinearIcon, NotionIcon } from '@/components/icons' + +interface ConnectorMeta { + icon: React.ComponentType<{ className?: string }> + name: string +} + +/** Connector type → client-safe metadata (icon + display name) */ +export const CONNECTOR_META: Record = { + confluence: { icon: ConfluenceIcon, name: 'Confluence' }, + github: { icon: GithubIcon, name: 'GitHub' }, + linear: { icon: LinearIcon, name: 'Linear' }, + notion: { icon: NotionIcon, name: 'Notion' }, +} + +/** Connector type → icon component mapping for client-side use */ +export const CONNECTOR_ICONS: Record< + string, + React.ComponentType<{ className?: string }> +> = Object.fromEntries(Object.entries(CONNECTOR_META).map(([k, v]) => [k, v.icon])) diff --git a/apps/sim/connectors/index.ts b/apps/sim/connectors/index.ts new file mode 100644 index 000000000..71f8b304e --- /dev/null +++ b/apps/sim/connectors/index.ts @@ -0,0 +1,9 @@ +export { CONNECTOR_REGISTRY } from '@/connectors/registry' +export type { + ConnectorConfig, + ConnectorConfigField, + ConnectorRegistry, + ExternalDocument, + ExternalDocumentList, + SyncResult, +} from '@/connectors/types' diff --git a/apps/sim/connectors/jira/index.ts b/apps/sim/connectors/jira/index.ts new file mode 100644 index 000000000..93d9a7a2a --- /dev/null +++ b/apps/sim/connectors/jira/index.ts @@ -0,0 +1 @@ +export { jiraConnector } from '@/connectors/jira/jira' diff --git a/apps/sim/connectors/jira/jira.ts b/apps/sim/connectors/jira/jira.ts new file mode 100644 index 000000000..185cf9574 --- /dev/null +++ b/apps/sim/connectors/jira/jira.ts @@ -0,0 +1,313 @@ +import { createLogger } from '@sim/logger' +import { JiraIcon } from '@/components/icons' +import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' +import { extractAdfText, getJiraCloudId } from '@/tools/jira/utils' + +const logger = createLogger('JiraConnector') + +const PAGE_SIZE = 50 + +/** + * Computes a SHA-256 hash of the given content. + */ +async function computeContentHash(content: string): Promise { + const data = new TextEncoder().encode(content) + const hashBuffer = await crypto.subtle.digest('SHA-256', data) + return Array.from(new Uint8Array(hashBuffer)) + .map((b) => b.toString(16).padStart(2, '0')) + .join('') +} + +/** + * Builds a plain-text representation of a Jira issue for knowledge base indexing. + */ +function buildIssueContent(fields: Record): string { + const parts: string[] = [] + + const summary = fields.summary as string | undefined + if (summary) parts.push(summary) + + const description = extractAdfText(fields.description) + if (description) parts.push(description) + + const comments = fields.comment as { comments?: Array<{ body?: unknown }> } | undefined + if (comments?.comments) { + for (const comment of comments.comments) { + const text = extractAdfText(comment.body) + if (text) parts.push(text) + } + } + + return parts.join('\n\n').trim() +} + +/** + * Converts a Jira issue API response to an ExternalDocument. + */ +async function issueToDocument( + issue: Record, + domain: string +): Promise { + const fields = (issue.fields || {}) as Record + const content = buildIssueContent(fields) + const contentHash = await computeContentHash(content) + + const key = issue.key as string + const issueType = fields.issuetype as Record | undefined + const status = fields.status as Record | undefined + const priority = fields.priority as Record | undefined + const assignee = fields.assignee as Record | undefined + const reporter = fields.reporter as Record | undefined + const project = fields.project as Record | undefined + const labels = Array.isArray(fields.labels) ? (fields.labels as string[]) : [] + + return { + externalId: String(issue.id), + title: `${key}: ${(fields.summary as string) || 'Untitled'}`, + content, + mimeType: 'text/plain', + sourceUrl: `https://${domain}/browse/${key}`, + contentHash, + metadata: { + key, + issueType: issueType?.name, + status: status?.name, + priority: priority?.name, + assignee: assignee?.displayName, + reporter: reporter?.displayName, + project: project?.key, + labels, + created: fields.created, + updated: fields.updated, + }, + } +} + +export const jiraConnector: ConnectorConfig = { + id: 'jira', + name: 'Jira', + description: 'Sync issues from a Jira project into your knowledge base', + version: '1.0.0', + icon: JiraIcon, + + oauth: { + required: true, + provider: 'jira', + requiredScopes: ['read:jira-work', 'offline_access'], + }, + + configFields: [ + { + id: 'domain', + title: 'Jira Domain', + type: 'short-input', + placeholder: 'yoursite.atlassian.net', + required: true, + }, + { + id: 'projectKey', + title: 'Project Key', + type: 'short-input', + placeholder: 'e.g. ENG, PROJ', + required: true, + }, + { + id: 'jql', + title: 'JQL Filter', + type: 'short-input', + required: false, + placeholder: 'e.g. status = "Done" AND type = Bug', + }, + { + id: 'maxIssues', + title: 'Max Issues', + type: 'short-input', + required: false, + placeholder: 'e.g. 500 (default: unlimited)', + }, + ], + + listDocuments: async ( + accessToken: string, + sourceConfig: Record, + cursor?: string + ): Promise => { + const domain = sourceConfig.domain as string + const projectKey = sourceConfig.projectKey as string + const jqlFilter = (sourceConfig.jql as string) || '' + const maxIssues = sourceConfig.maxIssues ? Number(sourceConfig.maxIssues) : 0 + + const cloudId = await getJiraCloudId(domain, accessToken) + + let jql = `project = "${projectKey}" ORDER BY updated DESC` + if (jqlFilter.trim()) { + jql = `project = "${projectKey}" AND (${jqlFilter.trim()}) ORDER BY updated DESC` + } + + const startAt = cursor ? Number(cursor) : 0 + + const params = new URLSearchParams() + params.append('jql', jql) + params.append('startAt', String(startAt)) + params.append('maxResults', String(PAGE_SIZE)) + params.append( + 'fields', + 'summary,description,comment,issuetype,status,priority,assignee,reporter,project,labels,created,updated' + ) + + const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/search?${params.toString()}` + + logger.info(`Listing Jira issues for project ${projectKey}`, { startAt }) + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorText = await response.text() + logger.error('Failed to search Jira issues', { + status: response.status, + error: errorText, + }) + throw new Error(`Failed to search Jira issues: ${response.status}`) + } + + const data = await response.json() + const issues = (data.issues || []) as Record[] + const total = (data.total as number) ?? 0 + + const documents: ExternalDocument[] = await Promise.all( + issues.map((issue) => issueToDocument(issue, domain)) + ) + + const nextStart = startAt + issues.length + const hasMore = nextStart < total && (maxIssues <= 0 || nextStart < maxIssues) + + return { + documents, + nextCursor: hasMore ? String(nextStart) : undefined, + hasMore, + } + }, + + getDocument: async ( + accessToken: string, + sourceConfig: Record, + externalId: string + ): Promise => { + const domain = sourceConfig.domain as string + const cloudId = await getJiraCloudId(domain, accessToken) + + const params = new URLSearchParams() + params.append( + 'fields', + 'summary,description,comment,issuetype,status,priority,assignee,reporter,project,labels,created,updated' + ) + + const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${externalId}?${params.toString()}` + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + if (response.status === 404) return null + throw new Error(`Failed to get Jira issue: ${response.status}`) + } + + const issue = await response.json() + return issueToDocument(issue, domain) + }, + + validateConfig: async ( + accessToken: string, + sourceConfig: Record + ): Promise<{ valid: boolean; error?: string }> => { + const domain = sourceConfig.domain as string + const projectKey = sourceConfig.projectKey as string + + if (!domain || !projectKey) { + return { valid: false, error: 'Domain and project key are required' } + } + + const maxIssues = sourceConfig.maxIssues as string | undefined + if (maxIssues && (Number.isNaN(Number(maxIssues)) || Number(maxIssues) <= 0)) { + return { valid: false, error: 'Max issues must be a positive number' } + } + + const jql = sourceConfig.jql as string | undefined + if (jql?.trim()) { + if (/\b(delete|drop|truncate|insert|update|alter|create|grant|revoke)\b/i.test(jql)) { + return { valid: false, error: 'Invalid JQL filter' } + } + } + + try { + const cloudId = await getJiraCloudId(domain, accessToken) + + // Verify the project exists by running a minimal search + const params = new URLSearchParams() + params.append('jql', `project = "${projectKey}"`) + params.append('maxResults', '0') + + const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/search?${params.toString()}` + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorText = await response.text() + if (response.status === 400) { + return { valid: false, error: `Project "${projectKey}" not found or JQL is invalid` } + } + return { valid: false, error: `Failed to validate: ${response.status} - ${errorText}` } + } + + return { valid: true } + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to validate configuration' + return { valid: false, error: message } + } + }, + + tagDefinitions: [ + { id: 'issueType', displayName: 'Issue Type', fieldType: 'text' }, + { id: 'status', displayName: 'Status', fieldType: 'text' }, + { id: 'priority', displayName: 'Priority', fieldType: 'text' }, + { id: 'labels', displayName: 'Labels', fieldType: 'text' }, + { id: 'assignee', displayName: 'Assignee', fieldType: 'text' }, + { id: 'updated', displayName: 'Last Updated', fieldType: 'date' }, + ], + + mapTags: (metadata: Record): Record => { + const result: Record = {} + + if (typeof metadata.issueType === 'string') result.issueType = metadata.issueType + if (typeof metadata.status === 'string') result.status = metadata.status + if (typeof metadata.priority === 'string') result.priority = metadata.priority + + const labels = Array.isArray(metadata.labels) ? (metadata.labels as string[]) : [] + if (labels.length > 0) result.labels = labels.join(', ') + + if (typeof metadata.assignee === 'string') result.assignee = metadata.assignee + + if (typeof metadata.updated === 'string') { + const date = new Date(metadata.updated) + if (!Number.isNaN(date.getTime())) result.updated = date + } + + return result + }, +} diff --git a/apps/sim/connectors/linear/index.ts b/apps/sim/connectors/linear/index.ts new file mode 100644 index 000000000..d74cf04a0 --- /dev/null +++ b/apps/sim/connectors/linear/index.ts @@ -0,0 +1 @@ +export { linearConnector } from '@/connectors/linear/linear' diff --git a/apps/sim/connectors/linear/linear.ts b/apps/sim/connectors/linear/linear.ts new file mode 100644 index 000000000..b39c16977 --- /dev/null +++ b/apps/sim/connectors/linear/linear.ts @@ -0,0 +1,415 @@ +import { createLogger } from '@sim/logger' +import { LinearIcon } from '@/components/icons' +import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' + +const logger = createLogger('LinearConnector') + +const LINEAR_API = 'https://api.linear.app/graphql' + +/** + * Computes a SHA-256 hash of the given content. + */ +async function computeContentHash(content: string): Promise { + const data = new TextEncoder().encode(content) + const hashBuffer = await crypto.subtle.digest('SHA-256', data) + return Array.from(new Uint8Array(hashBuffer)) + .map((b) => b.toString(16).padStart(2, '0')) + .join('') +} + +/** + * Strips Markdown formatting to produce plain text. + */ +function markdownToPlainText(md: string): string { + let text = md + .replace(/!\[.*?\]\(.*?\)/g, '') // images + .replace(/\[([^\]]*)\]\(.*?\)/g, '$1') // links + .replace(/#{1,6}\s+/g, '') // headings + .replace(/(\*\*|__)(.*?)\1/g, '$2') // bold + .replace(/(\*|_)(.*?)\1/g, '$2') // italic + .replace(/~~(.*?)~~/g, '$1') // strikethrough + .replace(/`{3}[\s\S]*?`{3}/g, '') // code blocks + .replace(/`([^`]*)`/g, '$1') // inline code + .replace(/^\s*[-*+]\s+/gm, '') // list items + .replace(/^\s*\d+\.\s+/gm, '') // ordered list items + .replace(/^\s*>\s+/gm, '') // blockquotes + .replace(/---+/g, '') // horizontal rules + text = text.replace(/\s+/g, ' ').trim() + return text +} + +/** + * Executes a GraphQL query against the Linear API. + */ +async function linearGraphQL( + accessToken: string, + query: string, + variables?: Record +): Promise> { + const response = await fetch(LINEAR_API, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ query, variables }), + }) + + if (!response.ok) { + const errorText = await response.text() + logger.error('Linear GraphQL request failed', { status: response.status, error: errorText }) + throw new Error(`Linear API error: ${response.status}`) + } + + const json = (await response.json()) as { data?: Record; errors?: unknown[] } + if (json.errors) { + logger.error('Linear GraphQL errors', { errors: json.errors }) + throw new Error(`Linear GraphQL error: ${JSON.stringify(json.errors)}`) + } + + return json.data as Record +} + +/** + * Builds a formatted text document from a Linear issue. + */ +function buildIssueContent(issue: Record): string { + const parts: string[] = [] + + const identifier = issue.identifier as string | undefined + const title = (issue.title as string) || 'Untitled' + parts.push(`${identifier ? `${identifier}: ` : ''}${title}`) + + const state = issue.state as Record | undefined + if (state?.name) parts.push(`Status: ${state.name}`) + + const priority = issue.priorityLabel as string | undefined + if (priority) parts.push(`Priority: ${priority}`) + + const assignee = issue.assignee as Record | undefined + if (assignee?.name) parts.push(`Assignee: ${assignee.name}`) + + const labelsConn = issue.labels as Record | undefined + const labelNodes = (labelsConn?.nodes || []) as Record[] + if (labelNodes.length > 0) { + parts.push(`Labels: ${labelNodes.map((l) => l.name as string).join(', ')}`) + } + + const description = issue.description as string | undefined + if (description) { + parts.push('') + parts.push(markdownToPlainText(description)) + } + + return parts.join('\n') +} + +const ISSUE_FIELDS = ` + id + identifier + title + description + priority + priorityLabel + url + createdAt + updatedAt + state { name } + assignee { name } + labels { nodes { name } } + team { name key } + project { name } +` + +const ISSUE_BY_ID_QUERY = ` + query GetIssue($id: String!) { + issue(id: $id) { + ${ISSUE_FIELDS} + } + } +` + +const TEAMS_QUERY = ` + query { teams { nodes { id name key } } } +` + +/** + * Dynamically builds a GraphQL issues query with only the filter clauses + * that have values, preventing null comparators from being sent to Linear. + */ +function buildIssuesQuery(sourceConfig: Record): { + query: string + variables: Record +} { + const teamId = (sourceConfig.teamId as string) || '' + const projectId = (sourceConfig.projectId as string) || '' + const stateFilter = (sourceConfig.stateFilter as string) || '' + + const varDefs: string[] = ['$first: Int!', '$after: String'] + const filterClauses: string[] = [] + const variables: Record = {} + + if (teamId) { + varDefs.push('$teamId: String!') + filterClauses.push('team: { id: { eq: $teamId } }') + variables.teamId = teamId + } + + if (projectId) { + varDefs.push('$projectId: String!') + filterClauses.push('project: { id: { eq: $projectId } }') + variables.projectId = projectId + } + + if (stateFilter) { + const states = stateFilter + .split(',') + .map((s) => s.trim()) + .filter(Boolean) + if (states.length > 0) { + varDefs.push('$stateFilter: [String!]!') + filterClauses.push('state: { name: { in: $stateFilter } }') + variables.stateFilter = states + } + } + + const filterArg = filterClauses.length > 0 ? `, filter: { ${filterClauses.join(', ')} }` : '' + + const query = ` + query ListIssues(${varDefs.join(', ')}) { + issues(first: $first, after: $after${filterArg}) { + nodes { + ${ISSUE_FIELDS} + } + pageInfo { + hasNextPage + endCursor + } + } + } + ` + + return { query, variables } +} + +export const linearConnector: ConnectorConfig = { + id: 'linear', + name: 'Linear', + description: 'Sync issues from Linear into your knowledge base', + version: '1.0.0', + icon: LinearIcon, + + oauth: { + required: true, + provider: 'linear', + requiredScopes: ['read'], + }, + + configFields: [ + { + id: 'teamId', + title: 'Team ID', + type: 'short-input', + placeholder: 'e.g. abc123 (leave empty for all teams)', + required: false, + }, + { + id: 'projectId', + title: 'Project ID', + type: 'short-input', + placeholder: 'e.g. def456 (leave empty for all projects)', + required: false, + }, + { + id: 'stateFilter', + title: 'State Filter', + type: 'short-input', + placeholder: 'e.g. In Progress, Todo', + required: false, + }, + { + id: 'maxIssues', + title: 'Max Issues', + type: 'short-input', + required: false, + placeholder: 'e.g. 500 (default: unlimited)', + }, + ], + + listDocuments: async ( + accessToken: string, + sourceConfig: Record, + cursor?: string + ): Promise => { + const maxIssues = sourceConfig.maxIssues ? Number(sourceConfig.maxIssues) : 0 + const pageSize = maxIssues > 0 ? Math.min(maxIssues, 50) : 50 + + const { query, variables } = buildIssuesQuery(sourceConfig) + const allVars = { ...variables, first: pageSize, after: cursor || undefined } + + logger.info('Listing Linear issues', { + cursor, + pageSize, + hasTeamFilter: Boolean(sourceConfig.teamId), + hasProjectFilter: Boolean(sourceConfig.projectId), + }) + + const data = await linearGraphQL(accessToken, query, allVars) + const issuesConn = data.issues as Record + const nodes = (issuesConn.nodes || []) as Record[] + const pageInfo = issuesConn.pageInfo as Record + + const documents: ExternalDocument[] = await Promise.all( + nodes.map(async (issue) => { + const content = buildIssueContent(issue) + const contentHash = await computeContentHash(content) + + const labelNodes = ((issue.labels as Record)?.nodes || []) as Record< + string, + unknown + >[] + + return { + externalId: issue.id as string, + title: `${(issue.identifier as string) || ''}: ${(issue.title as string) || 'Untitled'}`, + content, + mimeType: 'text/plain' as const, + sourceUrl: (issue.url as string) || undefined, + contentHash, + metadata: { + identifier: issue.identifier, + state: (issue.state as Record)?.name, + priority: issue.priorityLabel, + assignee: (issue.assignee as Record)?.name, + labels: labelNodes.map((l) => l.name as string), + team: (issue.team as Record)?.name, + project: (issue.project as Record)?.name, + lastModified: issue.updatedAt, + }, + } + }) + ) + + const hasNextPage = Boolean(pageInfo.hasNextPage) + const endCursor = (pageInfo.endCursor as string) || undefined + + return { + documents, + nextCursor: hasNextPage ? endCursor : undefined, + hasMore: hasNextPage, + } + }, + + getDocument: async ( + accessToken: string, + sourceConfig: Record, + externalId: string + ): Promise => { + try { + const data = await linearGraphQL(accessToken, ISSUE_BY_ID_QUERY, { id: externalId }) + const issue = data.issue as Record | null + + if (!issue) return null + + const content = buildIssueContent(issue) + const contentHash = await computeContentHash(content) + + const labelNodes = ((issue.labels as Record)?.nodes || []) as Record< + string, + unknown + >[] + + return { + externalId: issue.id as string, + title: `${(issue.identifier as string) || ''}: ${(issue.title as string) || 'Untitled'}`, + content, + mimeType: 'text/plain', + sourceUrl: (issue.url as string) || undefined, + contentHash, + metadata: { + identifier: issue.identifier, + state: (issue.state as Record)?.name, + priority: issue.priorityLabel, + assignee: (issue.assignee as Record)?.name, + labels: labelNodes.map((l) => l.name as string), + team: (issue.team as Record)?.name, + project: (issue.project as Record)?.name, + lastModified: issue.updatedAt, + }, + } + } catch (error) { + logger.error('Failed to get Linear issue', { + externalId, + error: error instanceof Error ? error.message : String(error), + }) + return null + } + }, + + validateConfig: async ( + accessToken: string, + sourceConfig: Record + ): Promise<{ valid: boolean; error?: string }> => { + const maxIssues = sourceConfig.maxIssues as string | undefined + if (maxIssues && (Number.isNaN(Number(maxIssues)) || Number(maxIssues) <= 0)) { + return { valid: false, error: 'Max issues must be a positive number' } + } + + try { + // Verify the token works by fetching teams + const data = await linearGraphQL(accessToken, TEAMS_QUERY) + const teamsConn = data.teams as Record + const teams = (teamsConn.nodes || []) as Record[] + + if (teams.length === 0) { + return { + valid: false, + error: 'No teams found — check that the OAuth token has read access', + } + } + + // If teamId specified, verify it exists + const teamId = sourceConfig.teamId as string | undefined + if (teamId) { + const found = teams.some((t) => t.id === teamId) + if (!found) { + return { + valid: false, + error: `Team ID "${teamId}" not found. Available teams: ${teams.map((t) => `${t.name} (${t.id})`).join(', ')}`, + } + } + } + + return { valid: true } + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to validate configuration' + return { valid: false, error: message } + } + }, + + tagDefinitions: [ + { id: 'labels', displayName: 'Labels', fieldType: 'text' }, + { id: 'state', displayName: 'State', fieldType: 'text' }, + { id: 'priority', displayName: 'Priority', fieldType: 'text' }, + { id: 'assignee', displayName: 'Assignee', fieldType: 'text' }, + { id: 'lastModified', displayName: 'Last Modified', fieldType: 'date' }, + ], + + mapTags: (metadata: Record): Record => { + const result: Record = {} + + const labels = Array.isArray(metadata.labels) ? (metadata.labels as string[]) : [] + if (labels.length > 0) result.labels = labels.join(', ') + + if (typeof metadata.state === 'string') result.state = metadata.state + if (typeof metadata.priority === 'string') result.priority = metadata.priority + if (typeof metadata.assignee === 'string') result.assignee = metadata.assignee + + if (typeof metadata.lastModified === 'string') { + const date = new Date(metadata.lastModified) + if (!Number.isNaN(date.getTime())) result.lastModified = date + } + + return result + }, +} diff --git a/apps/sim/connectors/notion/index.ts b/apps/sim/connectors/notion/index.ts new file mode 100644 index 000000000..00db08247 --- /dev/null +++ b/apps/sim/connectors/notion/index.ts @@ -0,0 +1 @@ +export { notionConnector } from '@/connectors/notion/notion' diff --git a/apps/sim/connectors/notion/notion.ts b/apps/sim/connectors/notion/notion.ts new file mode 100644 index 000000000..ac0799275 --- /dev/null +++ b/apps/sim/connectors/notion/notion.ts @@ -0,0 +1,587 @@ +import { createLogger } from '@sim/logger' +import { NotionIcon } from '@/components/icons' +import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' + +const logger = createLogger('NotionConnector') + +const NOTION_API_VERSION = '2022-06-28' +const NOTION_BASE_URL = 'https://api.notion.com/v1' + +/** + * Computes a SHA-256 hash of the given content. + */ +async function computeContentHash(content: string): Promise { + const data = new TextEncoder().encode(content) + const hashBuffer = await crypto.subtle.digest('SHA-256', data) + return Array.from(new Uint8Array(hashBuffer)) + .map((b) => b.toString(16).padStart(2, '0')) + .join('') +} + +/** + * Extracts the title from a Notion page's properties. + */ +function extractTitle(properties: Record): string { + for (const value of Object.values(properties)) { + const prop = value as Record + if (prop.type === 'title' && Array.isArray(prop.title) && prop.title.length > 0) { + return prop.title.map((t: Record) => (t.plain_text as string) || '').join('') + } + } + return 'Untitled' +} + +/** + * Extracts plain text from a rich_text array. + */ +function richTextToPlain(richText: Record[]): string { + return richText.map((t) => (t.plain_text as string) || '').join('') +} + +/** + * Extracts plain text content from Notion blocks. + */ +function blocksToPlainText(blocks: Record[]): string { + return blocks + .map((block) => { + const type = block.type as string + const blockData = block[type] as Record | undefined + if (!blockData) return '' + + const richText = blockData.rich_text as Record[] | undefined + if (!richText) return '' + + const text = richTextToPlain(richText) + + switch (type) { + case 'heading_1': + return `# ${text}` + case 'heading_2': + return `## ${text}` + case 'heading_3': + return `### ${text}` + case 'bulleted_list_item': + return `- ${text}` + case 'numbered_list_item': + return `1. ${text}` + case 'to_do': { + const checked = (blockData.checked as boolean) ? '[x]' : '[ ]' + return `${checked} ${text}` + } + case 'quote': + return `> ${text}` + case 'callout': + return text + case 'toggle': + return text + default: + return text + } + }) + .filter(Boolean) + .join('\n\n') +} + +/** + * Fetches all block children for a page, handling pagination. + */ +async function fetchAllBlocks( + accessToken: string, + pageId: string +): Promise[]> { + const allBlocks: Record[] = [] + let cursor: string | undefined + let hasMore = true + + while (hasMore) { + const params = new URLSearchParams({ page_size: '100' }) + if (cursor) params.append('start_cursor', cursor) + + const response = await fetch( + `${NOTION_BASE_URL}/blocks/${pageId}/children?${params.toString()}`, + { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Notion-Version': NOTION_API_VERSION, + }, + } + ) + + if (!response.ok) { + logger.warn(`Failed to fetch blocks for page ${pageId}`, { status: response.status }) + break + } + + const data = await response.json() + allBlocks.push(...(data.results || [])) + cursor = data.next_cursor ?? undefined + hasMore = data.has_more === true + } + + return allBlocks +} + +/** + * Extracts multi_select tags from page properties. + */ +function extractTags(properties: Record): string[] { + const tags: string[] = [] + for (const value of Object.values(properties)) { + const prop = value as Record + if (prop.type === 'multi_select' && Array.isArray(prop.multi_select)) { + for (const item of prop.multi_select) { + const name = (item as Record).name as string + if (name) tags.push(name) + } + } + if (prop.type === 'select' && prop.select) { + const name = (prop.select as Record).name as string + if (name) tags.push(name) + } + } + return tags +} + +/** + * Converts a Notion page to an ExternalDocument by fetching its block content. + */ +async function pageToExternalDocument( + accessToken: string, + page: Record +): Promise { + const pageId = page.id as string + const properties = (page.properties || {}) as Record + const title = extractTitle(properties) + const url = page.url as string + + // Fetch page content + const blocks = await fetchAllBlocks(accessToken, pageId) + const plainText = blocksToPlainText(blocks) + const contentHash = await computeContentHash(plainText) + + // Extract tags from multi_select/select properties + const tags = extractTags(properties) + + return { + externalId: pageId, + title: title || 'Untitled', + content: plainText, + mimeType: 'text/plain', + sourceUrl: url, + contentHash, + metadata: { + tags, + lastModified: page.last_edited_time as string, + createdTime: page.created_time as string, + parentType: (page.parent as Record)?.type, + }, + } +} + +export const notionConnector: ConnectorConfig = { + id: 'notion', + name: 'Notion', + description: 'Sync pages from a Notion workspace into your knowledge base', + version: '1.0.0', + icon: NotionIcon, + + oauth: { + required: true, + provider: 'notion', + requiredScopes: [], + }, + + configFields: [ + { + id: 'scope', + title: 'Sync Scope', + type: 'dropdown', + required: false, + options: [ + { label: 'Entire workspace', id: 'workspace' }, + { label: 'Specific database', id: 'database' }, + { label: 'Specific page (and children)', id: 'page' }, + ], + }, + { + id: 'databaseId', + title: 'Database ID', + type: 'short-input', + required: false, + placeholder: 'e.g. 8a3b5f6e-1234-5678-abcd-ef0123456789', + }, + { + id: 'rootPageId', + title: 'Page ID', + type: 'short-input', + required: false, + placeholder: 'e.g. 8a3b5f6e-1234-5678-abcd-ef0123456789', + }, + { + id: 'searchQuery', + title: 'Search Filter', + type: 'short-input', + required: false, + placeholder: 'e.g. meeting notes, project plan', + }, + { + id: 'maxPages', + title: 'Max Pages', + type: 'short-input', + required: false, + placeholder: 'e.g. 500 (default: unlimited)', + }, + ], + + listDocuments: async ( + accessToken: string, + sourceConfig: Record, + cursor?: string + ): Promise => { + const scope = (sourceConfig.scope as string) || 'workspace' + const databaseId = (sourceConfig.databaseId as string)?.trim() + const rootPageId = (sourceConfig.rootPageId as string)?.trim() + const maxPages = sourceConfig.maxPages ? Number(sourceConfig.maxPages) : 0 + + if (scope === 'database' && databaseId) { + return listFromDatabase(accessToken, databaseId, maxPages, cursor) + } + + if (scope === 'page' && rootPageId) { + return listFromParentPage(accessToken, rootPageId, maxPages, cursor) + } + + // Default: workspace-wide search + const searchQuery = (sourceConfig.searchQuery as string) || '' + return listFromWorkspace(accessToken, searchQuery, maxPages, cursor) + }, + + getDocument: async ( + accessToken: string, + _sourceConfig: Record, + externalId: string + ): Promise => { + const response = await fetch(`${NOTION_BASE_URL}/pages/${externalId}`, { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Notion-Version': NOTION_API_VERSION, + }, + }) + + if (!response.ok) { + if (response.status === 404) return null + throw new Error(`Failed to get Notion page: ${response.status}`) + } + + const page = await response.json() + return pageToExternalDocument(accessToken, page) + }, + + validateConfig: async ( + accessToken: string, + sourceConfig: Record + ): Promise<{ valid: boolean; error?: string }> => { + const scope = (sourceConfig.scope as string) || 'workspace' + const databaseId = (sourceConfig.databaseId as string)?.trim() + const rootPageId = (sourceConfig.rootPageId as string)?.trim() + const maxPages = sourceConfig.maxPages as string | undefined + + if (maxPages && (Number.isNaN(Number(maxPages)) || Number(maxPages) <= 0)) { + return { valid: false, error: 'Max pages must be a positive number' } + } + + if (scope === 'database' && !databaseId) { + return { valid: false, error: 'Database ID is required when scope is "Specific database"' } + } + + if (scope === 'page' && !rootPageId) { + return { valid: false, error: 'Page ID is required when scope is "Specific page"' } + } + + try { + // Verify the token works + if (scope === 'database' && databaseId) { + // Verify database is accessible + const response = await fetch(`${NOTION_BASE_URL}/databases/${databaseId}`, { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Notion-Version': NOTION_API_VERSION, + }, + }) + if (!response.ok) { + return { valid: false, error: `Cannot access database: ${response.status}` } + } + } else if (scope === 'page' && rootPageId) { + // Verify page is accessible + const response = await fetch(`${NOTION_BASE_URL}/pages/${rootPageId}`, { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Notion-Version': NOTION_API_VERSION, + }, + }) + if (!response.ok) { + return { valid: false, error: `Cannot access page: ${response.status}` } + } + } else { + // Workspace scope — just verify token works + const response = await fetch(`${NOTION_BASE_URL}/search`, { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Notion-Version': NOTION_API_VERSION, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ page_size: 1 }), + }) + if (!response.ok) { + const errorText = await response.text() + return { valid: false, error: `Cannot access Notion workspace: ${errorText}` } + } + } + + return { valid: true } + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to validate configuration' + return { valid: false, error: message } + } + }, + + tagDefinitions: [ + { id: 'tags', displayName: 'Tags', fieldType: 'text' }, + { id: 'lastModified', displayName: 'Last Modified', fieldType: 'date' }, + { id: 'created', displayName: 'Created', fieldType: 'date' }, + ], + + mapTags: (metadata: Record): Record => { + const result: Record = {} + + const tags = Array.isArray(metadata.tags) ? (metadata.tags as string[]) : [] + if (tags.length > 0) result.tags = tags.join(', ') + + if (typeof metadata.lastModified === 'string') { + const date = new Date(metadata.lastModified) + if (!Number.isNaN(date.getTime())) result.lastModified = date + } + + if (typeof metadata.createdTime === 'string') { + const date = new Date(metadata.createdTime) + if (!Number.isNaN(date.getTime())) result.created = date + } + + return result + }, +} + +/** + * Lists pages from the entire workspace using the search API. + */ +async function listFromWorkspace( + accessToken: string, + searchQuery: string, + maxPages: number, + cursor?: string +): Promise { + const body: Record = { + page_size: 20, + filter: { value: 'page', property: 'object' }, + sort: { direction: 'descending', timestamp: 'last_edited_time' }, + } + + if (searchQuery.trim()) { + body.query = searchQuery.trim() + } + + if (cursor) { + body.start_cursor = cursor + } + + logger.info('Listing Notion pages from workspace', { searchQuery, cursor }) + + const response = await fetch(`${NOTION_BASE_URL}/search`, { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Notion-Version': NOTION_API_VERSION, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }) + + if (!response.ok) { + const errorText = await response.text() + logger.error('Failed to search Notion', { status: response.status, error: errorText }) + throw new Error(`Failed to search Notion: ${response.status}`) + } + + const data = await response.json() + const results = (data.results || []) as Record[] + const pages = results.filter((r) => r.object === 'page' && !(r.archived as boolean)) + + const documents = await processPages(accessToken, pages) + const nextCursor = (data.next_cursor as string) ?? undefined + + return { + documents, + nextCursor, + hasMore: data.has_more === true && (maxPages <= 0 || documents.length < maxPages), + } +} + +/** + * Lists pages from a specific Notion database. + */ +async function listFromDatabase( + accessToken: string, + databaseId: string, + maxPages: number, + cursor?: string +): Promise { + const body: Record = { + page_size: 20, + } + + if (cursor) { + body.start_cursor = cursor + } + + logger.info('Querying Notion database', { databaseId, cursor }) + + const response = await fetch(`${NOTION_BASE_URL}/databases/${databaseId}/query`, { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Notion-Version': NOTION_API_VERSION, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }) + + if (!response.ok) { + const errorText = await response.text() + logger.error('Failed to query Notion database', { status: response.status, error: errorText }) + throw new Error(`Failed to query Notion database: ${response.status}`) + } + + const data = await response.json() + const results = (data.results || []) as Record[] + const pages = results.filter((r) => r.object === 'page' && !(r.archived as boolean)) + + const documents = await processPages(accessToken, pages) + const nextCursor = (data.next_cursor as string) ?? undefined + + return { + documents, + nextCursor, + hasMore: data.has_more === true && (maxPages <= 0 || documents.length < maxPages), + } +} + +/** + * Lists child pages under a specific parent page. + * + * Uses the blocks children endpoint to find child_page blocks, + * then fetches each page's content. + */ +async function listFromParentPage( + accessToken: string, + rootPageId: string, + maxPages: number, + cursor?: string +): Promise { + const params = new URLSearchParams({ page_size: '100' }) + if (cursor) params.append('start_cursor', cursor) + + logger.info('Listing child pages under root page', { rootPageId, cursor }) + + const response = await fetch( + `${NOTION_BASE_URL}/blocks/${rootPageId}/children?${params.toString()}`, + { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Notion-Version': NOTION_API_VERSION, + }, + } + ) + + if (!response.ok) { + const errorText = await response.text() + logger.error('Failed to list child blocks', { status: response.status, error: errorText }) + throw new Error(`Failed to list child blocks: ${response.status}`) + } + + const data = await response.json() + const blocks = (data.results || []) as Record[] + + // Filter to child_page and child_database blocks + const childPageIds = blocks + .filter((b) => b.type === 'child_page' || b.type === 'child_database') + .map((b) => b.id as string) + + // Also include the root page itself on the first call (no cursor) + const pageIdsToFetch = !cursor ? [rootPageId, ...childPageIds] : childPageIds + + // Fetch each child page + const documents: ExternalDocument[] = [] + for (const pageId of pageIdsToFetch) { + if (maxPages > 0 && documents.length >= maxPages) break + + try { + const pageResponse = await fetch(`${NOTION_BASE_URL}/pages/${pageId}`, { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Notion-Version': NOTION_API_VERSION, + }, + }) + + if (!pageResponse.ok) { + logger.warn(`Failed to fetch child page ${pageId}`, { status: pageResponse.status }) + continue + } + + const page = await pageResponse.json() + if (page.archived) continue + + const doc = await pageToExternalDocument(accessToken, page) + documents.push(doc) + } catch (error) { + logger.warn(`Failed to process child page ${pageId}`, { + error: error instanceof Error ? error.message : String(error), + }) + } + } + + const nextCursor = (data.next_cursor as string) ?? undefined + + return { + documents, + nextCursor, + hasMore: data.has_more === true && (maxPages <= 0 || documents.length < maxPages), + } +} + +/** + * Converts an array of Notion page objects to ExternalDocuments. + */ +async function processPages( + accessToken: string, + pages: Record[] +): Promise { + const documents: ExternalDocument[] = [] + for (const page of pages) { + try { + const doc = await pageToExternalDocument(accessToken, page) + documents.push(doc) + } catch (error) { + logger.warn(`Failed to process Notion page ${page.id}`, { + error: error instanceof Error ? error.message : String(error), + }) + } + } + return documents +} diff --git a/apps/sim/connectors/registry.ts b/apps/sim/connectors/registry.ts new file mode 100644 index 000000000..a47d1c850 --- /dev/null +++ b/apps/sim/connectors/registry.ts @@ -0,0 +1,18 @@ +import { airtableConnector } from '@/connectors/airtable' +import { confluenceConnector } from '@/connectors/confluence' +import { githubConnector } from '@/connectors/github' +import { googleDriveConnector } from '@/connectors/google-drive' +import { jiraConnector } from '@/connectors/jira' +import { linearConnector } from '@/connectors/linear' +import { notionConnector } from '@/connectors/notion' +import type { ConnectorRegistry } from '@/connectors/types' + +export const CONNECTOR_REGISTRY: ConnectorRegistry = { + airtable: airtableConnector, + confluence: confluenceConnector, + github: githubConnector, + google_drive: googleDriveConnector, + jira: jiraConnector, + linear: linearConnector, + notion: notionConnector, +} diff --git a/apps/sim/connectors/types.ts b/apps/sim/connectors/types.ts new file mode 100644 index 000000000..366086b1f --- /dev/null +++ b/apps/sim/connectors/types.ts @@ -0,0 +1,159 @@ +import type { OAuthService } from '@/lib/oauth/types' + +/** + * A single document fetched from an external source. + */ +export interface ExternalDocument { + /** Source-specific unique ID (page ID, file ID) */ + externalId: string + /** Document title / filename */ + title: string + /** Extracted text content */ + content: string + /** MIME type of the content */ + mimeType: string + /** Link back to the original document */ + sourceUrl?: string + /** SHA-256 of content for change detection */ + contentHash: string + /** Additional source-specific metadata */ + metadata?: Record +} + +/** + * Paginated result from listing documents in an external source. + */ +export interface ExternalDocumentList { + documents: ExternalDocument[] + nextCursor?: string + hasMore: boolean +} + +/** + * Result of a sync operation. + */ +export interface SyncResult { + docsAdded: number + docsUpdated: number + docsDeleted: number + docsUnchanged: number + error?: string +} + +/** + * Config field for source-specific settings (rendered in the add-connector UI). + */ +export interface ConnectorConfigField { + id: string + title: string + type: 'short-input' | 'dropdown' + placeholder?: string + required?: boolean + description?: string + options?: { label: string; id: string }[] +} + +/** + * Declarative config for a knowledge source connector. + * + * Mirrors ToolConfig/TriggerConfig pattern: + * - Purely declarative metadata (id, name, icon, oauth, configFields) + * - Runtime functions for data fetching (listDocuments, getDocument, validateConfig) + * + * Adding a new connector = creating one of these + registering it. + */ +export interface ConnectorConfig { + /** Unique connector identifier, e.g. 'confluence', 'google_drive', 'notion' */ + id: string + /** Human-readable name, e.g. 'Confluence', 'Google Drive' */ + name: string + /** Short description of the connector */ + description: string + /** Semver version */ + version: string + /** Icon component for the connector */ + icon: React.ComponentType<{ className?: string }> + + /** OAuth configuration (same pattern as ToolConfig.oauth) */ + oauth: { + required: true + provider: OAuthService + requiredScopes?: string[] + } + + /** Source configuration fields rendered in the add-connector UI */ + configFields: ConnectorConfigField[] + + /** List all documents from the configured source (handles pagination via cursor) */ + listDocuments: ( + accessToken: string, + sourceConfig: Record, + cursor?: string + ) => Promise + + /** Fetch a single document by its external ID */ + getDocument: ( + accessToken: string, + sourceConfig: Record, + externalId: string + ) => Promise + + /** Validate that sourceConfig is correct and accessible (called on save) */ + validateConfig: ( + accessToken: string, + sourceConfig: Record + ) => Promise<{ valid: boolean; error?: string }> + + /** Map source metadata to semantic tag keys (translated to slots by the sync engine) */ + mapTags?: (metadata: Record) => Record + + /** + * Tag definitions this connector populates. Shown in the add-connector modal + * as opt-out checkboxes. On connector creation, tag definitions are auto-created + * on the KB for enabled slots, and mapTags output is filtered to only include them. + */ + tagDefinitions?: ConnectorTagDefinition[] +} + +/** + * A tag that a connector populates, with a semantic ID and human-readable name. + * Slots are dynamically assigned on connector creation via getNextAvailableSlot. + */ +export interface ConnectorTagDefinition { + /** Semantic ID matching a key returned by mapTags (e.g. 'labels', 'version') */ + id: string + /** Human-readable name shown in UI (e.g. 'Labels', 'Last Modified') */ + displayName: string + /** Field type determines which slot pool to draw from */ + fieldType: 'text' | 'number' | 'date' | 'boolean' +} + +/** + * Tag slots available on the document table for connector metadata mapping. + */ +export interface DocumentTags { + tag1?: string + tag2?: string + tag3?: string + tag4?: string + tag5?: string + tag6?: string + tag7?: string + number1?: number + number2?: number + number3?: number + number4?: number + number5?: number + date1?: Date + date2?: Date + boolean1?: boolean + boolean2?: boolean + boolean3?: boolean +} + +/** + * Registry mapping connector IDs to their configs. + */ +export interface ConnectorRegistry { + [connectorId: string]: ConnectorConfig +} diff --git a/apps/sim/hooks/kb/use-knowledge-base-tag-definitions.ts b/apps/sim/hooks/kb/use-knowledge-base-tag-definitions.ts index 0c1541b2a..1a90410bb 100644 --- a/apps/sim/hooks/kb/use-knowledge-base-tag-definitions.ts +++ b/apps/sim/hooks/kb/use-knowledge-base-tag-definitions.ts @@ -3,7 +3,7 @@ import { useCallback } from 'react' import { useQueryClient } from '@tanstack/react-query' import type { AllTagSlot } from '@/lib/knowledge/constants' -import { knowledgeKeys, useTagDefinitionsQuery } from '@/hooks/queries/knowledge' +import { knowledgeKeys, useTagDefinitionsQuery } from '@/hooks/queries/kb/knowledge' export interface TagDefinition { id: string diff --git a/apps/sim/hooks/kb/use-knowledge.ts b/apps/sim/hooks/kb/use-knowledge.ts index 738e87ae6..39bfbeb06 100644 --- a/apps/sim/hooks/kb/use-knowledge.ts +++ b/apps/sim/hooks/kb/use-knowledge.ts @@ -2,6 +2,7 @@ import { useCallback, useMemo } from 'react' import { useQueryClient } from '@tanstack/react-query' import type { ChunkData, DocumentData, KnowledgeBaseData } from '@/lib/knowledge/types' import { + type DocumentTagFilter, type KnowledgeChunksResponse, type KnowledgeDocumentsResponse, knowledgeKeys, @@ -12,7 +13,7 @@ import { useKnowledgeBasesQuery, useKnowledgeChunksQuery, useKnowledgeDocumentsQuery, -} from '@/hooks/queries/knowledge' +} from '@/hooks/queries/kb/knowledge' const DEFAULT_PAGE_SIZE = 50 @@ -72,12 +73,14 @@ export function useKnowledgeBaseDocuments( | false | ((data: KnowledgeDocumentsResponse | undefined) => number | false) enabledFilter?: 'all' | 'enabled' | 'disabled' + tagFilters?: DocumentTagFilter[] } ) { const queryClient = useQueryClient() const requestLimit = options?.limit ?? DEFAULT_PAGE_SIZE const requestOffset = options?.offset ?? 0 const enabledFilter = options?.enabledFilter ?? 'all' + const tagFilters = options?.tagFilters const paramsKey = serializeDocumentParams({ knowledgeBaseId, limit: requestLimit, @@ -86,6 +89,7 @@ export function useKnowledgeBaseDocuments( sortBy: options?.sortBy, sortOrder: options?.sortOrder, enabledFilter, + tagFilters, }) const refetchIntervalFn = useMemo(() => { @@ -107,6 +111,7 @@ export function useKnowledgeBaseDocuments( sortBy: options?.sortBy, sortOrder: options?.sortOrder, enabledFilter, + tagFilters, }, { enabled: (options?.enabled ?? true) && Boolean(knowledgeBaseId), @@ -227,7 +232,8 @@ export function useDocumentChunks( knowledgeBaseId: string, documentId: string, page = 1, - search = '' + search = '', + enabledFilter: 'all' | 'enabled' | 'disabled' = 'all' ) { const queryClient = useQueryClient() @@ -241,6 +247,7 @@ export function useDocumentChunks( limit: DEFAULT_PAGE_SIZE, offset, search: search || undefined, + enabledFilter, }, { enabled: Boolean(knowledgeBaseId && documentId), @@ -272,11 +279,12 @@ export function useDocumentChunks( limit: DEFAULT_PAGE_SIZE, offset, search: search || undefined, + enabledFilter, }) await queryClient.invalidateQueries({ queryKey: knowledgeKeys.chunks(knowledgeBaseId, documentId, paramsKey), }) - }, [knowledgeBaseId, documentId, offset, search, queryClient]) + }, [knowledgeBaseId, documentId, offset, search, enabledFilter, queryClient]) const updateChunk = useCallback( (chunkId: string, updates: Partial) => { @@ -286,6 +294,7 @@ export function useDocumentChunks( limit: DEFAULT_PAGE_SIZE, offset, search: search || undefined, + enabledFilter, }) queryClient.setQueryData( knowledgeKeys.chunks(knowledgeBaseId, documentId, paramsKey), @@ -300,7 +309,7 @@ export function useDocumentChunks( } ) }, - [knowledgeBaseId, documentId, offset, search, queryClient] + [knowledgeBaseId, documentId, offset, search, enabledFilter, queryClient] ) return { diff --git a/apps/sim/hooks/kb/use-tag-definitions.ts b/apps/sim/hooks/kb/use-tag-definitions.ts index d84132b0d..f328c5f57 100644 --- a/apps/sim/hooks/kb/use-tag-definitions.ts +++ b/apps/sim/hooks/kb/use-tag-definitions.ts @@ -9,7 +9,7 @@ import { useDeleteDocumentTagDefinitions, useDocumentTagDefinitionsQuery, useSaveDocumentTagDefinitions, -} from '@/hooks/queries/knowledge' +} from '@/hooks/queries/kb/knowledge' export interface TagDefinition { id: string diff --git a/apps/sim/hooks/queries/kb/connectors.ts b/apps/sim/hooks/queries/kb/connectors.ts new file mode 100644 index 000000000..8541de52e --- /dev/null +++ b/apps/sim/hooks/queries/kb/connectors.ts @@ -0,0 +1,426 @@ +import { createLogger } from '@sim/logger' +import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { knowledgeKeys } from '@/hooks/queries/kb/knowledge' + +const logger = createLogger('KnowledgeConnectorQueries') + +export interface ConnectorData { + id: string + knowledgeBaseId: string + connectorType: string + credentialId: string + sourceConfig: Record + syncMode: string + syncIntervalMinutes: number + status: 'active' | 'paused' | 'syncing' | 'error' + lastSyncAt: string | null + lastSyncError: string | null + lastSyncDocCount: number | null + nextSyncAt: string | null + consecutiveFailures: number + createdAt: string + updatedAt: string +} + +export interface SyncLogData { + id: string + connectorId: string + status: string + startedAt: string + completedAt: string | null + docsAdded: number + docsUpdated: number + docsDeleted: number + docsUnchanged: number + errorMessage: string | null +} + +export interface ConnectorDetailData extends ConnectorData { + syncLogs: SyncLogData[] +} + +export const connectorKeys = { + all: (knowledgeBaseId: string) => + [...knowledgeKeys.detail(knowledgeBaseId), 'connectors'] as const, + list: (knowledgeBaseId?: string) => + [...knowledgeKeys.detail(knowledgeBaseId), 'connectors', 'list'] as const, + detail: (knowledgeBaseId?: string, connectorId?: string) => + [...knowledgeKeys.detail(knowledgeBaseId), 'connectors', 'detail', connectorId ?? ''] as const, +} + +async function fetchConnectors(knowledgeBaseId: string): Promise { + const response = await fetch(`/api/knowledge/${knowledgeBaseId}/connectors`) + + if (!response.ok) { + throw new Error(`Failed to fetch connectors: ${response.status}`) + } + + const result = await response.json() + if (!result?.success) { + throw new Error(result?.error || 'Failed to fetch connectors') + } + + return Array.isArray(result.data) ? result.data : [] +} + +async function fetchConnectorDetail( + knowledgeBaseId: string, + connectorId: string +): Promise { + const response = await fetch(`/api/knowledge/${knowledgeBaseId}/connectors/${connectorId}`) + + if (!response.ok) { + throw new Error(`Failed to fetch connector: ${response.status}`) + } + + const result = await response.json() + if (!result?.success || !result?.data) { + throw new Error(result?.error || 'Failed to fetch connector') + } + + return result.data +} + +export function useConnectorList(knowledgeBaseId?: string) { + return useQuery({ + queryKey: connectorKeys.list(knowledgeBaseId), + queryFn: () => fetchConnectors(knowledgeBaseId as string), + enabled: Boolean(knowledgeBaseId), + staleTime: 30 * 1000, + placeholderData: keepPreviousData, + refetchInterval: (query) => { + const connectors = query.state.data + const hasSyncing = connectors?.some((c) => c.status === 'syncing') + return hasSyncing ? 3000 : false + }, + }) +} + +export function useConnectorDetail(knowledgeBaseId?: string, connectorId?: string) { + return useQuery({ + queryKey: connectorKeys.detail(knowledgeBaseId, connectorId), + queryFn: () => fetchConnectorDetail(knowledgeBaseId as string, connectorId as string), + enabled: Boolean(knowledgeBaseId && connectorId), + staleTime: 30 * 1000, + placeholderData: keepPreviousData, + }) +} + +export interface CreateConnectorParams { + knowledgeBaseId: string + connectorType: string + credentialId: string + sourceConfig: Record + syncIntervalMinutes?: number +} + +async function createConnector({ + knowledgeBaseId, + ...body +}: CreateConnectorParams): Promise { + const response = await fetch(`/api/knowledge/${knowledgeBaseId}/connectors`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }) + + if (!response.ok) { + const result = await response.json() + throw new Error(result.error || 'Failed to create connector') + } + + const result = await response.json() + if (!result?.success || !result?.data) { + throw new Error(result?.error || 'Failed to create connector') + } + + return result.data +} + +export function useCreateConnector() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: createConnector, + onSuccess: (_, { knowledgeBaseId }) => { + queryClient.invalidateQueries({ + queryKey: connectorKeys.list(knowledgeBaseId), + }) + }, + }) +} + +export interface UpdateConnectorParams { + knowledgeBaseId: string + connectorId: string + updates: { + sourceConfig?: Record + syncIntervalMinutes?: number + status?: 'active' | 'paused' + } +} + +async function updateConnector({ + knowledgeBaseId, + connectorId, + updates, +}: UpdateConnectorParams): Promise { + const response = await fetch(`/api/knowledge/${knowledgeBaseId}/connectors/${connectorId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updates), + }) + + if (!response.ok) { + const result = await response.json() + throw new Error(result.error || 'Failed to update connector') + } + + const result = await response.json() + if (!result?.success) { + throw new Error(result?.error || 'Failed to update connector') + } + + return result.data +} + +export function useUpdateConnector() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: updateConnector, + onSuccess: (_, { knowledgeBaseId, connectorId }) => { + queryClient.invalidateQueries({ + queryKey: connectorKeys.list(knowledgeBaseId), + }) + queryClient.invalidateQueries({ + queryKey: connectorKeys.detail(knowledgeBaseId, connectorId), + }) + }, + }) +} + +export interface DeleteConnectorParams { + knowledgeBaseId: string + connectorId: string +} + +async function deleteConnector({ + knowledgeBaseId, + connectorId, +}: DeleteConnectorParams): Promise { + const response = await fetch(`/api/knowledge/${knowledgeBaseId}/connectors/${connectorId}`, { + method: 'DELETE', + }) + + if (!response.ok) { + const result = await response.json() + throw new Error(result.error || 'Failed to delete connector') + } + + const result = await response.json() + if (!result?.success) { + throw new Error(result?.error || 'Failed to delete connector') + } +} + +export function useDeleteConnector() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: deleteConnector, + onSuccess: (_, { knowledgeBaseId }) => { + queryClient.invalidateQueries({ + queryKey: connectorKeys.list(knowledgeBaseId), + }) + queryClient.invalidateQueries({ + queryKey: knowledgeKeys.detail(knowledgeBaseId), + }) + }, + }) +} + +export interface TriggerSyncParams { + knowledgeBaseId: string + connectorId: string +} + +async function triggerSync({ knowledgeBaseId, connectorId }: TriggerSyncParams): Promise { + const response = await fetch(`/api/knowledge/${knowledgeBaseId}/connectors/${connectorId}/sync`, { + method: 'POST', + }) + + if (!response.ok) { + const result = await response.json() + throw new Error(result.error || 'Failed to trigger sync') + } +} + +export function useTriggerSync() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: triggerSync, + onSuccess: (_, { knowledgeBaseId, connectorId }) => { + queryClient.invalidateQueries({ + queryKey: connectorKeys.list(knowledgeBaseId), + }) + queryClient.invalidateQueries({ + queryKey: connectorKeys.detail(knowledgeBaseId, connectorId), + }) + queryClient.invalidateQueries({ + queryKey: knowledgeKeys.detail(knowledgeBaseId), + }) + }, + }) +} + +export interface ConnectorDocumentData { + id: string + filename: string + externalId: string | null + sourceUrl: string | null + enabled: boolean + deletedAt: string | null + userExcluded: boolean + uploadedAt: string + processingStatus: string +} + +export interface ConnectorDocumentsResponse { + documents: ConnectorDocumentData[] + counts: { active: number; excluded: number } +} + +export const connectorDocumentKeys = { + list: (knowledgeBaseId?: string, connectorId?: string) => + [...connectorKeys.detail(knowledgeBaseId, connectorId), 'documents'] as const, +} + +async function fetchConnectorDocuments( + knowledgeBaseId: string, + connectorId: string, + includeExcluded: boolean +): Promise { + const params = includeExcluded ? '?includeExcluded=true' : '' + const response = await fetch( + `/api/knowledge/${knowledgeBaseId}/connectors/${connectorId}/documents${params}` + ) + + if (!response.ok) { + throw new Error(`Failed to fetch connector documents: ${response.status}`) + } + + const result = await response.json() + if (!result?.success) { + throw new Error(result?.error || 'Failed to fetch connector documents') + } + + return result.data +} + +export function useConnectorDocuments( + knowledgeBaseId?: string, + connectorId?: string, + options?: { includeExcluded?: boolean } +) { + return useQuery({ + queryKey: [ + ...connectorDocumentKeys.list(knowledgeBaseId, connectorId), + options?.includeExcluded ?? false, + ], + queryFn: () => + fetchConnectorDocuments( + knowledgeBaseId as string, + connectorId as string, + options?.includeExcluded ?? false + ), + enabled: Boolean(knowledgeBaseId && connectorId), + staleTime: 30 * 1000, + placeholderData: keepPreviousData, + }) +} + +interface ConnectorDocumentMutationParams { + knowledgeBaseId: string + connectorId: string + documentIds: string[] +} + +async function excludeConnectorDocuments({ + knowledgeBaseId, + connectorId, + documentIds, +}: ConnectorDocumentMutationParams): Promise<{ excludedCount: number }> { + const response = await fetch( + `/api/knowledge/${knowledgeBaseId}/connectors/${connectorId}/documents`, + { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ operation: 'exclude', documentIds }), + } + ) + + if (!response.ok) { + const result = await response.json() + throw new Error(result.error || 'Failed to exclude documents') + } + + const result = await response.json() + return result.data +} + +export function useExcludeConnectorDocument() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: excludeConnectorDocuments, + onSuccess: (_, { knowledgeBaseId, connectorId }) => { + queryClient.invalidateQueries({ + queryKey: connectorDocumentKeys.list(knowledgeBaseId, connectorId), + }) + queryClient.invalidateQueries({ + queryKey: knowledgeKeys.detail(knowledgeBaseId), + }) + }, + }) +} + +async function restoreConnectorDocuments({ + knowledgeBaseId, + connectorId, + documentIds, +}: ConnectorDocumentMutationParams): Promise<{ restoredCount: number }> { + const response = await fetch( + `/api/knowledge/${knowledgeBaseId}/connectors/${connectorId}/documents`, + { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ operation: 'restore', documentIds }), + } + ) + + if (!response.ok) { + const result = await response.json() + throw new Error(result.error || 'Failed to restore documents') + } + + const result = await response.json() + return result.data +} + +export function useRestoreConnectorDocument() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: restoreConnectorDocuments, + onSuccess: (_, { knowledgeBaseId, connectorId }) => { + queryClient.invalidateQueries({ + queryKey: connectorDocumentKeys.list(knowledgeBaseId, connectorId), + }) + queryClient.invalidateQueries({ + queryKey: knowledgeKeys.detail(knowledgeBaseId), + }) + }, + }) +} diff --git a/apps/sim/hooks/queries/knowledge.ts b/apps/sim/hooks/queries/kb/knowledge.ts similarity index 98% rename from apps/sim/hooks/queries/knowledge.ts rename to apps/sim/hooks/queries/kb/knowledge.ts index 2d5edb7a7..04573e6b1 100644 --- a/apps/sim/hooks/queries/knowledge.ts +++ b/apps/sim/hooks/queries/kb/knowledge.ts @@ -79,6 +79,14 @@ export async function fetchDocument( return result.data } +export interface DocumentTagFilter { + tagSlot: string + fieldType: 'text' | 'number' | 'date' | 'boolean' + operator: string + value: string + valueTo?: string +} + export interface KnowledgeDocumentsParams { knowledgeBaseId: string search?: string @@ -87,6 +95,7 @@ export interface KnowledgeDocumentsParams { sortBy?: string sortOrder?: string enabledFilter?: 'all' | 'enabled' | 'disabled' + tagFilters?: DocumentTagFilter[] } export interface KnowledgeDocumentsResponse { @@ -102,6 +111,7 @@ export async function fetchKnowledgeDocuments({ sortBy, sortOrder, enabledFilter, + tagFilters, }: KnowledgeDocumentsParams): Promise { const params = new URLSearchParams() if (search) params.set('search', search) @@ -110,6 +120,7 @@ export async function fetchKnowledgeDocuments({ params.set('limit', limit.toString()) params.set('offset', offset.toString()) if (enabledFilter) params.set('enabledFilter', enabledFilter) + if (tagFilters && tagFilters.length > 0) params.set('tagFilters', JSON.stringify(tagFilters)) const url = `/api/knowledge/${knowledgeBaseId}/documents${params.toString() ? `?${params.toString()}` : ''}` const response = await fetch(url) @@ -147,6 +158,7 @@ export interface KnowledgeChunksParams { knowledgeBaseId: string documentId: string search?: string + enabledFilter?: 'all' | 'enabled' | 'disabled' limit?: number offset?: number } @@ -160,11 +172,15 @@ export async function fetchKnowledgeChunks({ knowledgeBaseId, documentId, search, + enabledFilter, limit = 50, offset = 0, }: KnowledgeChunksParams): Promise { const params = new URLSearchParams() if (search) params.set('search', search) + if (enabledFilter && enabledFilter !== 'all') { + params.set('enabled', enabledFilter === 'enabled' ? 'true' : 'false') + } if (limit) params.set('limit', limit.toString()) if (offset) params.set('offset', offset.toString()) @@ -234,6 +250,7 @@ export const serializeDocumentParams = (params: KnowledgeDocumentsParams) => sortBy: params.sortBy ?? '', sortOrder: params.sortOrder ?? '', enabledFilter: params.enabledFilter ?? 'all', + tagFilters: params.tagFilters ?? [], }) export function useKnowledgeDocumentsQuery( @@ -260,6 +277,7 @@ export function useKnowledgeDocumentsQuery( export const serializeChunkParams = (params: KnowledgeChunksParams) => JSON.stringify({ search: params.search ?? '', + enabledFilter: params.enabledFilter ?? 'all', limit: params.limit ?? 50, offset: params.offset ?? 0, }) diff --git a/apps/sim/hooks/queries/oauth-connections.ts b/apps/sim/hooks/queries/oauth/oauth-connections.ts similarity index 100% rename from apps/sim/hooks/queries/oauth-connections.ts rename to apps/sim/hooks/queries/oauth/oauth-connections.ts diff --git a/apps/sim/hooks/queries/oauth-credentials.ts b/apps/sim/hooks/queries/oauth/oauth-credentials.ts similarity index 100% rename from apps/sim/hooks/queries/oauth-credentials.ts rename to apps/sim/hooks/queries/oauth/oauth-credentials.ts diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index be5b961f0..f5df643af 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -1814,7 +1814,13 @@ export const auth = betterAuth({ authorizationUrl: 'https://airtable.com/oauth2/v1/authorize', tokenUrl: 'https://airtable.com/oauth2/v1/token', userInfoUrl: 'https://api.airtable.com/v0/meta/whoami', - scopes: ['data.records:read', 'data.records:write', 'user.email:read', 'webhook:manage'], + scopes: [ + 'data.records:read', + 'data.records:write', + 'schema.bases:read', + 'user.email:read', + 'webhook:manage', + ], responseType: 'code', pkce: true, accessType: 'offline', diff --git a/apps/sim/lib/knowledge/connectors/sync-engine.ts b/apps/sim/lib/knowledge/connectors/sync-engine.ts new file mode 100644 index 000000000..092cdc0ac --- /dev/null +++ b/apps/sim/lib/knowledge/connectors/sync-engine.ts @@ -0,0 +1,493 @@ +import { db } from '@sim/db' +import { + document, + knowledgeBase, + knowledgeConnector, + knowledgeConnectorSyncLog, +} from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, eq, isNull, ne } from 'drizzle-orm' +import { getInternalApiBaseUrl } from '@/lib/core/utils/urls' +import { isTriggerAvailable, processDocumentAsync } from '@/lib/knowledge/documents/service' +import { StorageService } from '@/lib/uploads' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' +import { knowledgeConnectorSync } from '@/background/knowledge-connector-sync' +import { CONNECTOR_REGISTRY } from '@/connectors/registry' +import type { DocumentTags, ExternalDocument, SyncResult } from '@/connectors/types' + +const logger = createLogger('ConnectorSyncEngine') + +/** + * Resolves tag values from connector metadata using the connector's mapTags function. + * Translates semantic keys returned by mapTags to actual DB slots using the + * tagSlotMapping stored in sourceConfig during connector creation. + */ +function resolveTagMapping( + connectorType: string, + metadata: Record, + sourceConfig?: Record +): Partial | undefined { + const config = CONNECTOR_REGISTRY[connectorType] + if (!config?.mapTags || !metadata) return undefined + + const semanticTags = config.mapTags(metadata) + const mapping = sourceConfig?.tagSlotMapping as Record | undefined + if (!mapping || !semanticTags) return undefined + + const result: Partial = {} + for (const [semanticKey, slot] of Object.entries(mapping)) { + const value = semanticTags[semanticKey] + ;(result as Record)[slot] = value != null ? value : null + } + return result +} + +/** + * Dispatch a connector sync — uses Trigger.dev when available, + * otherwise falls back to direct executeSync. + */ +export async function dispatchSync( + connectorId: string, + options?: { fullSync?: boolean; requestId?: string } +): Promise { + const requestId = options?.requestId ?? crypto.randomUUID() + + if (isTriggerAvailable()) { + await knowledgeConnectorSync.trigger({ + connectorId, + fullSync: options?.fullSync, + requestId, + }) + logger.info(`Dispatched connector sync to Trigger.dev`, { connectorId, requestId }) + } else { + executeSync(connectorId, { fullSync: options?.fullSync }).catch((error) => { + logger.error(`Sync failed for connector ${connectorId}`, { + error: error instanceof Error ? error.message : String(error), + requestId, + }) + }) + } +} + +/** + * Execute a sync for a given knowledge connector. + * + * This is the core sync algorithm — connector-agnostic. + * It looks up the ConnectorConfig from the registry and calls its + * listDocuments/getDocument methods. + */ +export async function executeSync( + connectorId: string, + options?: { fullSync?: boolean } +): Promise { + const result: SyncResult = { + docsAdded: 0, + docsUpdated: 0, + docsDeleted: 0, + docsUnchanged: 0, + } + + const connectorRows = await db + .select() + .from(knowledgeConnector) + .where(and(eq(knowledgeConnector.id, connectorId), isNull(knowledgeConnector.deletedAt))) + .limit(1) + + if (connectorRows.length === 0) { + throw new Error(`Connector not found: ${connectorId}`) + } + + const connector = connectorRows[0] + + const connectorConfig = CONNECTOR_REGISTRY[connector.connectorType] + if (!connectorConfig) { + throw new Error(`Unknown connector type: ${connector.connectorType}`) + } + + const kbRows = await db + .select({ userId: knowledgeBase.userId }) + .from(knowledgeBase) + .where(eq(knowledgeBase.id, connector.knowledgeBaseId)) + .limit(1) + + if (kbRows.length === 0) { + throw new Error(`Knowledge base not found: ${connector.knowledgeBaseId}`) + } + + const userId = kbRows[0].userId + + const accessToken = await refreshAccessTokenIfNeeded( + connector.credentialId, + userId, + `sync-${connectorId}` + ) + + if (!accessToken) { + throw new Error('Failed to obtain access token') + } + + const lockResult = await db + .update(knowledgeConnector) + .set({ status: 'syncing', updatedAt: new Date() }) + .where(and(eq(knowledgeConnector.id, connectorId), ne(knowledgeConnector.status, 'syncing'))) + .returning({ id: knowledgeConnector.id }) + + if (lockResult.length === 0) { + logger.info('Sync already in progress, skipping', { connectorId }) + return result + } + + const syncLogId = crypto.randomUUID() + await db.insert(knowledgeConnectorSyncLog).values({ + id: syncLogId, + connectorId, + status: 'started', + startedAt: new Date(), + }) + + const sourceConfig = connector.sourceConfig as Record + + try { + const externalDocs: ExternalDocument[] = [] + let cursor: string | undefined + let hasMore = true + const MAX_PAGES = 500 + + for (let pageNum = 0; hasMore && pageNum < MAX_PAGES; pageNum++) { + const page = await connectorConfig.listDocuments(accessToken, sourceConfig, cursor) + externalDocs.push(...page.documents) + + if (page.hasMore && !page.nextCursor) { + logger.warn('Source returned hasMore=true with no cursor, stopping pagination', { + connectorId, + pageNum, + docsSoFar: externalDocs.length, + }) + break + } + + cursor = page.nextCursor + hasMore = page.hasMore + } + + logger.info(`Fetched ${externalDocs.length} documents from ${connectorConfig.name}`, { + connectorId, + }) + + const existingDocs = await db + .select({ + id: document.id, + externalId: document.externalId, + contentHash: document.contentHash, + }) + .from(document) + .where(and(eq(document.connectorId, connectorId), isNull(document.deletedAt))) + + const excludedDocs = await db + .select({ externalId: document.externalId }) + .from(document) + .where(and(eq(document.connectorId, connectorId), eq(document.userExcluded, true))) + + const excludedExternalIds = new Set(excludedDocs.map((d) => d.externalId).filter(Boolean)) + + if (externalDocs.length === 0 && existingDocs.length > 0 && !options?.fullSync) { + logger.warn( + `Source returned 0 documents but ${existingDocs.length} exist — skipping reconciliation`, + { connectorId } + ) + + await db + .update(knowledgeConnectorSyncLog) + .set({ status: 'completed', completedAt: new Date() }) + .where(eq(knowledgeConnectorSyncLog.id, syncLogId)) + + const now = new Date() + const nextSync = + connector.syncIntervalMinutes > 0 + ? new Date(now.getTime() + connector.syncIntervalMinutes * 60 * 1000) + : null + + await db + .update(knowledgeConnector) + .set({ + status: 'active', + lastSyncAt: now, + lastSyncError: null, + nextSyncAt: nextSync, + consecutiveFailures: 0, + updatedAt: now, + }) + .where(eq(knowledgeConnector.id, connectorId)) + + return result + } + + const existingByExternalId = new Map( + existingDocs.filter((d) => d.externalId !== null).map((d) => [d.externalId!, d]) + ) + + const seenExternalIds = new Set() + + for (const extDoc of externalDocs) { + seenExternalIds.add(extDoc.externalId) + + if (excludedExternalIds.has(extDoc.externalId)) { + result.docsUnchanged++ + continue + } + + if (!extDoc.content.trim()) { + logger.info(`Skipping empty document: ${extDoc.title}`, { + externalId: extDoc.externalId, + }) + continue + } + + const existing = existingByExternalId.get(extDoc.externalId) + + if (!existing) { + await addDocument( + connector.knowledgeBaseId, + connectorId, + connector.connectorType, + extDoc, + sourceConfig + ) + result.docsAdded++ + } else if (existing.contentHash !== extDoc.contentHash) { + await updateDocument( + existing.id, + connector.knowledgeBaseId, + connectorId, + connector.connectorType, + extDoc, + sourceConfig + ) + result.docsUpdated++ + } else { + result.docsUnchanged++ + } + } + + if (options?.fullSync || connector.syncMode === 'incremental') { + for (const existing of existingDocs) { + if (existing.externalId && !seenExternalIds.has(existing.externalId)) { + await db + .update(document) + .set({ deletedAt: new Date() }) + .where(eq(document.id, existing.id)) + result.docsDeleted++ + } + } + } + + await db + .update(knowledgeConnectorSyncLog) + .set({ + status: 'completed', + completedAt: new Date(), + docsAdded: result.docsAdded, + docsUpdated: result.docsUpdated, + docsDeleted: result.docsDeleted, + docsUnchanged: result.docsUnchanged, + }) + .where(eq(knowledgeConnectorSyncLog.id, syncLogId)) + + const now = new Date() + const nextSync = + connector.syncIntervalMinutes > 0 + ? new Date(now.getTime() + connector.syncIntervalMinutes * 60 * 1000) + : null + + await db + .update(knowledgeConnector) + .set({ + status: 'active', + lastSyncAt: now, + lastSyncError: null, + lastSyncDocCount: externalDocs.length, + nextSyncAt: nextSync, + consecutiveFailures: 0, + updatedAt: now, + }) + .where(eq(knowledgeConnector.id, connectorId)) + + logger.info('Sync completed', { connectorId, ...result }) + return result + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + + await db + .update(knowledgeConnectorSyncLog) + .set({ + status: 'failed', + completedAt: new Date(), + errorMessage, + docsAdded: result.docsAdded, + docsUpdated: result.docsUpdated, + docsDeleted: result.docsDeleted, + docsUnchanged: result.docsUnchanged, + }) + .where(eq(knowledgeConnectorSyncLog.id, syncLogId)) + + const now = new Date() + const failures = (connector.consecutiveFailures ?? 0) + 1 + const backoffMinutes = Math.min(failures * 30, 1440) + const nextSync = new Date(now.getTime() + backoffMinutes * 60 * 1000) + + await db + .update(knowledgeConnector) + .set({ + status: 'error', + lastSyncAt: now, + lastSyncError: errorMessage, + nextSyncAt: nextSync, + consecutiveFailures: failures, + updatedAt: now, + }) + .where(eq(knowledgeConnector.id, connectorId)) + + logger.error('Sync failed', { connectorId, error: errorMessage }) + result.error = errorMessage + return result + } +} + +/** + * Upload content to storage as a .txt file, create a document record, + * and trigger processing via the existing pipeline. + */ +async function addDocument( + knowledgeBaseId: string, + connectorId: string, + connectorType: string, + extDoc: ExternalDocument, + sourceConfig?: Record +): Promise { + const documentId = crypto.randomUUID() + const contentBuffer = Buffer.from(extDoc.content, 'utf-8') + const safeTitle = extDoc.title.replace(/[^a-zA-Z0-9.-]/g, '_') + const customKey = `kb/${Date.now()}-${documentId}-${safeTitle}.txt` + + const fileInfo = await StorageService.uploadFile({ + file: contentBuffer, + fileName: `${safeTitle}.txt`, + contentType: 'text/plain', + context: 'knowledge-base', + customKey, + preserveKey: true, + }) + + const fileUrl = `${getInternalApiBaseUrl()}${fileInfo.path}?context=knowledge-base` + + const tagValues = extDoc.metadata + ? resolveTagMapping(connectorType, extDoc.metadata, sourceConfig) + : undefined + + const displayName = extDoc.title + const processingFilename = `${safeTitle}.txt` + + await db.insert(document).values({ + id: documentId, + knowledgeBaseId, + filename: displayName, + fileUrl, + fileSize: contentBuffer.length, + mimeType: 'text/plain', + chunkCount: 0, + tokenCount: 0, + characterCount: 0, + processingStatus: 'pending', + enabled: true, + connectorId, + externalId: extDoc.externalId, + contentHash: extDoc.contentHash, + sourceUrl: extDoc.sourceUrl ?? null, + ...tagValues, + uploadedAt: new Date(), + }) + + processDocumentAsync( + knowledgeBaseId, + documentId, + { + filename: processingFilename, + fileUrl, + fileSize: contentBuffer.length, + mimeType: 'text/plain', + }, + {} + ).catch((error) => { + logger.error('Failed to process connector document', { + documentId, + connectorId, + error: error instanceof Error ? error.message : String(error), + }) + }) +} + +/** + * Update an existing connector-sourced document with new content. + * Updates in-place to avoid unique constraint violations on (connectorId, externalId). + */ +async function updateDocument( + existingDocId: string, + knowledgeBaseId: string, + connectorId: string, + connectorType: string, + extDoc: ExternalDocument, + sourceConfig?: Record +): Promise { + const contentBuffer = Buffer.from(extDoc.content, 'utf-8') + const safeTitle = extDoc.title.replace(/[^a-zA-Z0-9.-]/g, '_') + const customKey = `kb/${Date.now()}-${existingDocId}-${safeTitle}.txt` + + const fileInfo = await StorageService.uploadFile({ + file: contentBuffer, + fileName: `${safeTitle}.txt`, + contentType: 'text/plain', + context: 'knowledge-base', + customKey, + preserveKey: true, + }) + + const fileUrl = `${getInternalApiBaseUrl()}${fileInfo.path}?context=knowledge-base` + + const tagValues = extDoc.metadata + ? resolveTagMapping(connectorType, extDoc.metadata, sourceConfig) + : undefined + + const processingFilename = `${safeTitle}.txt` + + await db + .update(document) + .set({ + filename: extDoc.title, + fileUrl, + fileSize: contentBuffer.length, + contentHash: extDoc.contentHash, + sourceUrl: extDoc.sourceUrl ?? null, + ...tagValues, + processingStatus: 'pending', + uploadedAt: new Date(), + }) + .where(eq(document.id, existingDocId)) + + processDocumentAsync( + knowledgeBaseId, + existingDocId, + { + filename: processingFilename, + fileUrl, + fileSize: contentBuffer.length, + mimeType: 'text/plain', + }, + {} + ).catch((error) => { + logger.error('Failed to re-process updated connector document', { + documentId: existingDocId, + connectorId, + error: error instanceof Error ? error.message : String(error), + }) + }) +} diff --git a/apps/sim/lib/knowledge/documents/service.ts b/apps/sim/lib/knowledge/documents/service.ts index cd4210b55..8af312487 100644 --- a/apps/sim/lib/knowledge/documents/service.ts +++ b/apps/sim/lib/knowledge/documents/service.ts @@ -1,9 +1,29 @@ import crypto, { randomUUID } from 'crypto' import { db } from '@sim/db' -import { document, embedding, knowledgeBase, knowledgeBaseTagDefinitions } from '@sim/db/schema' +import { + document, + embedding, + knowledgeBase, + knowledgeBaseTagDefinitions, + knowledgeConnector, +} from '@sim/db/schema' import { createLogger } from '@sim/logger' import { tasks } from '@trigger.dev/sdk' -import { and, asc, desc, eq, inArray, isNull, sql } from 'drizzle-orm' +import { + and, + asc, + desc, + eq, + gt, + gte, + inArray, + isNull, + lt, + lte, + ne, + type SQL, + sql, +} from 'drizzle-orm' import { env } from '@/lib/core/config/env' import { getStorageMethod, isRedisStorage } from '@/lib/core/storage' import { processDocument } from '@/lib/knowledge/documents/document-processor' @@ -770,6 +790,117 @@ export async function createDocumentRecords( }) } +/** + * A single tag filter condition passed from the API layer. + */ +export interface TagFilterCondition { + tagSlot: string + fieldType: 'text' | 'number' | 'date' | 'boolean' + operator: string + value: string + valueTo?: string +} + +/** + * Builds a Drizzle SQL condition from a tag filter. + */ +function buildTagFilterCondition(filter: TagFilterCondition): SQL | undefined { + const column = document[filter.tagSlot as keyof typeof document.$inferSelect] + if (!column) return undefined + + const col = document[filter.tagSlot as keyof typeof document] + + if (filter.fieldType === 'text') { + const v = filter.value + switch (filter.operator) { + case 'eq': + return eq(col as typeof document.tag1, v) + case 'neq': + return ne(col as typeof document.tag1, v) + case 'contains': + return sql`LOWER(${col}) LIKE LOWER(${`%${v}%`})` + case 'not_contains': + return sql`LOWER(${col}) NOT LIKE LOWER(${`%${v}%`})` + case 'starts_with': + return sql`LOWER(${col}) LIKE LOWER(${`${v}%`})` + case 'ends_with': + return sql`LOWER(${col}) LIKE LOWER(${`%${v}`})` + default: + return undefined + } + } + + if (filter.fieldType === 'number') { + const num = Number(filter.value) + if (Number.isNaN(num)) return undefined + switch (filter.operator) { + case 'eq': + return eq(col as typeof document.number1, num) + case 'neq': + return ne(col as typeof document.number1, num) + case 'gt': + return gt(col as typeof document.number1, num) + case 'gte': + return gte(col as typeof document.number1, num) + case 'lt': + return lt(col as typeof document.number1, num) + case 'lte': + return lte(col as typeof document.number1, num) + case 'between': { + const numTo = Number(filter.valueTo) + if (Number.isNaN(numTo)) return undefined + return and( + gte(col as typeof document.number1, num), + lte(col as typeof document.number1, numTo) + ) + } + default: + return undefined + } + } + + if (filter.fieldType === 'date') { + const v = filter.value + switch (filter.operator) { + case 'eq': + return eq(col as typeof document.date1, new Date(v)) + case 'neq': + return ne(col as typeof document.date1, new Date(v)) + case 'gt': + return gt(col as typeof document.date1, new Date(v)) + case 'gte': + return gte(col as typeof document.date1, new Date(v)) + case 'lt': + return lt(col as typeof document.date1, new Date(v)) + case 'lte': + return lte(col as typeof document.date1, new Date(v)) + case 'between': { + if (!filter.valueTo) return undefined + return and( + gte(col as typeof document.date1, new Date(v)), + lte(col as typeof document.date1, new Date(filter.valueTo)) + ) + } + default: + return undefined + } + } + + if (filter.fieldType === 'boolean') { + const boolVal = filter.value === 'true' + switch (filter.operator) { + case 'eq': + return eq(col as typeof document.boolean1, boolVal) + case 'neq': + return ne(col as typeof document.boolean1, boolVal) + default: + return undefined + } + } + + return undefined +} + /** * Get documents for a knowledge base with filtering and pagination */ @@ -782,6 +913,7 @@ export async function getDocuments( offset?: number sortBy?: DocumentSortField sortOrder?: SortOrder + tagFilters?: TagFilterCondition[] }, requestId: string ): Promise<{ @@ -821,6 +953,10 @@ export async function getDocuments( boolean1: boolean | null boolean2: boolean | null boolean3: boolean | null + // Connector fields + connectorId: string | null + connectorType: string | null + sourceUrl: string | null }> pagination: { total: number @@ -836,9 +972,10 @@ export async function getDocuments( offset = 0, sortBy = 'filename', sortOrder = 'asc', + tagFilters, } = options - const whereConditions = [ + const whereConditions: (SQL | undefined)[] = [ eq(document.knowledgeBaseId, knowledgeBaseId), isNull(document.deletedAt), ] @@ -853,6 +990,15 @@ export async function getDocuments( whereConditions.push(sql`LOWER(${document.filename}) LIKE LOWER(${`%${search}%`})`) } + if (tagFilters && tagFilters.length > 0) { + for (const filter of tagFilters) { + const condition = buildTagFilterCondition(filter) + if (condition) { + whereConditions.push(condition) + } + } + } + const totalResult = await db .select({ count: sql`COUNT(*)` }) .from(document) @@ -923,8 +1069,13 @@ export async function getDocuments( boolean1: document.boolean1, boolean2: document.boolean2, boolean3: document.boolean3, + // Connector fields + connectorId: document.connectorId, + connectorType: knowledgeConnector.connectorType, + sourceUrl: document.sourceUrl, }) .from(document) + .leftJoin(knowledgeConnector, eq(document.connectorId, knowledgeConnector.id)) .where(and(...whereConditions)) .orderBy(primaryOrderBy, secondaryOrderBy) .limit(limit) @@ -971,6 +1122,10 @@ export async function getDocuments( boolean1: doc.boolean1, boolean2: doc.boolean2, boolean3: doc.boolean3, + // Connector fields + connectorId: doc.connectorId, + connectorType: doc.connectorType ?? null, + sourceUrl: doc.sourceUrl, })), pagination: { total, @@ -1177,6 +1332,7 @@ export async function bulkDocumentOperation( .update(document) .set({ deletedAt: new Date(), + userExcluded: sql`CASE WHEN ${document.connectorId} IS NOT NULL THEN true ELSE ${document.userExcluded} END`, }) .where( and( @@ -1260,6 +1416,7 @@ export async function bulkDocumentOperationByFilter( .update(document) .set({ deletedAt: new Date(), + userExcluded: sql`CASE WHEN ${document.connectorId} IS NOT NULL THEN true ELSE ${document.userExcluded} END`, }) .where(and(...whereConditions)) .returning({ id: document.id, deletedAt: document.deletedAt }) @@ -1630,20 +1787,31 @@ export async function updateDocument( } /** - * Soft delete a document + * Soft delete a document. + * For connector-sourced documents, also sets userExcluded so the sync engine + * will not re-import the document on future syncs. */ export async function deleteDocument( documentId: string, requestId: string ): Promise<{ success: boolean; message: string }> { + const doc = await db + .select({ connectorId: document.connectorId }) + .from(document) + .where(eq(document.id, documentId)) + .limit(1) + await db .update(document) .set({ deletedAt: new Date(), + ...(doc[0]?.connectorId ? { userExcluded: true } : {}), }) .where(eq(document.id, documentId)) - logger.info(`[${requestId}] Document deleted: ${documentId}`) + logger.info(`[${requestId}] Document deleted: ${documentId}`, { + userExcluded: Boolean(doc[0]?.connectorId), + }) return { success: true, diff --git a/apps/sim/lib/knowledge/service.ts b/apps/sim/lib/knowledge/service.ts index 863a39b5c..9c07078d3 100644 --- a/apps/sim/lib/knowledge/service.ts +++ b/apps/sim/lib/knowledge/service.ts @@ -1,8 +1,8 @@ import { randomUUID } from 'crypto' import { db } from '@sim/db' -import { document, knowledgeBase, permissions } from '@sim/db/schema' +import { document, knowledgeBase, knowledgeConnector, permissions } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, count, eq, isNotNull, isNull, or } from 'drizzle-orm' +import { and, count, eq, inArray, isNotNull, isNull, or } from 'drizzle-orm' import type { ChunkingConfig, CreateKnowledgeBaseData, @@ -69,10 +69,38 @@ export async function getKnowledgeBases( .groupBy(knowledgeBase.id) .orderBy(knowledgeBase.createdAt) + const kbIds = knowledgeBasesWithCounts.map((kb) => kb.id) + + const connectorRows = + kbIds.length > 0 + ? await db + .select({ + knowledgeBaseId: knowledgeConnector.knowledgeBaseId, + connectorType: knowledgeConnector.connectorType, + }) + .from(knowledgeConnector) + .where( + and( + inArray(knowledgeConnector.knowledgeBaseId, kbIds), + isNull(knowledgeConnector.deletedAt) + ) + ) + : [] + + const connectorTypesByKb = new Map() + for (const row of connectorRows) { + const types = connectorTypesByKb.get(row.knowledgeBaseId) ?? [] + if (!types.includes(row.connectorType)) { + types.push(row.connectorType) + } + connectorTypesByKb.set(row.knowledgeBaseId, types) + } + return knowledgeBasesWithCounts.map((kb) => ({ ...kb, chunkingConfig: kb.chunkingConfig as ChunkingConfig, docCount: Number(kb.docCount), + connectorTypes: connectorTypesByKb.get(kb.id) ?? [], })) } @@ -122,6 +150,7 @@ export async function createKnowledgeBase( updatedAt: now, workspaceId: data.workspaceId, docCount: 0, + connectorTypes: [], } } @@ -203,6 +232,7 @@ export async function updateKnowledgeBase( ...updatedKb[0], chunkingConfig: updatedKb[0].chunkingConfig as ChunkingConfig, docCount: Number(updatedKb[0].docCount), + connectorTypes: [], } } @@ -243,6 +273,7 @@ export async function getKnowledgeBaseById( ...result[0], chunkingConfig: result[0].chunkingConfig as ChunkingConfig, docCount: Number(result[0].docCount), + connectorTypes: [], } } diff --git a/apps/sim/lib/knowledge/types.ts b/apps/sim/lib/knowledge/types.ts index f962a65fb..a7b629c68 100644 --- a/apps/sim/lib/knowledge/types.ts +++ b/apps/sim/lib/knowledge/types.ts @@ -27,6 +27,7 @@ export interface KnowledgeBaseWithCounts { updatedAt: Date workspaceId: string | null docCount: number + connectorTypes: string[] } export interface CreateKnowledgeBaseData { @@ -124,6 +125,7 @@ export interface KnowledgeBaseData { createdAt: string updatedAt: string workspaceId?: string + connectorTypes?: string[] } /** Document data for API responses */ @@ -160,6 +162,9 @@ export interface DocumentData { boolean1?: boolean | null boolean2?: boolean | null boolean3?: boolean | null + connectorId?: string | null + connectorType?: string | null + sourceUrl?: string | null } /** Chunk data for API responses */ diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 7bb11ca35..e5c1363f1 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -412,7 +412,13 @@ export const OAUTH_PROVIDERS: Record = { providerId: 'airtable', icon: AirtableIcon, baseProviderIcon: AirtableIcon, - scopes: ['data.records:read', 'data.records:write', 'user.email:read', 'webhook:manage'], + scopes: [ + 'data.records:read', + 'data.records:write', + 'schema.bases:read', + 'user.email:read', + 'webhook:manage', + ], }, }, defaultService: 'airtable', diff --git a/apps/sim/lib/workflows/comparison/resolve-values.ts b/apps/sim/lib/workflows/comparison/resolve-values.ts index 491265402..7c5d83ef1 100644 --- a/apps/sim/lib/workflows/comparison/resolve-values.ts +++ b/apps/sim/lib/workflows/comparison/resolve-values.ts @@ -3,7 +3,7 @@ import { getBlock } from '@/blocks/registry' import { SELECTOR_TYPES_HYDRATION_REQUIRED, type SubBlockConfig } from '@/blocks/types' import { CREDENTIAL_SET, isUuid } from '@/executor/constants' import { fetchCredentialSetById } from '@/hooks/queries/credential-sets' -import { fetchOAuthCredentialDetail } from '@/hooks/queries/oauth-credentials' +import { fetchOAuthCredentialDetail } from '@/hooks/queries/oauth/oauth-credentials' import { getSelectorDefinition } from '@/hooks/selectors/registry' import { resolveSelectorForSubBlock } from '@/hooks/selectors/resolution' import type { SelectorKey } from '@/hooks/selectors/types' diff --git a/apps/sim/tools/airtable/get_base_schema.ts b/apps/sim/tools/airtable/get_base_schema.ts new file mode 100644 index 000000000..5481b8990 --- /dev/null +++ b/apps/sim/tools/airtable/get_base_schema.ts @@ -0,0 +1,126 @@ +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface AirtableGetBaseSchemaParams { + accessToken: string + baseId: string +} + +interface AirtableFieldSchema { + id: string + name: string + type: string + description?: string + options?: Record +} + +interface AirtableViewSchema { + id: string + name: string + type: string +} + +interface AirtableTableSchema { + id: string + name: string + description?: string + fields: AirtableFieldSchema[] + views: AirtableViewSchema[] +} + +export interface AirtableGetBaseSchemaResponse extends ToolResponse { + output: { + tables: AirtableTableSchema[] + metadata: { + totalTables: number + } + } +} + +export const airtableGetBaseSchemaTool: ToolConfig< + AirtableGetBaseSchemaParams, + AirtableGetBaseSchemaResponse +> = { + id: 'airtable_get_base_schema', + name: 'Airtable Get Base Schema', + description: 'Get the schema of all tables, fields, and views in an Airtable base', + version: '1.0.0', + + oauth: { + required: true, + provider: 'airtable', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + baseId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Airtable base ID (starts with "app", e.g., "appXXXXXXXXXXXXXX")', + }, + }, + + request: { + url: (params) => `https://api.airtable.com/v0/meta/bases/${params.baseId}/tables`, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + const tables = (data.tables || []).map((table: Record) => ({ + id: table.id, + name: table.name, + description: table.description, + fields: ((table.fields as Record[]) || []).map((field) => ({ + id: field.id, + name: field.name, + type: field.type, + description: field.description, + options: field.options, + })), + views: ((table.views as Record[]) || []).map((view) => ({ + id: view.id, + name: view.name, + type: view.type, + })), + })) + return { + success: true, + output: { + tables, + metadata: { + totalTables: tables.length, + }, + }, + } + }, + + outputs: { + tables: { + type: 'json', + description: 'Array of table schemas with fields and views', + items: { + type: 'object', + properties: { + id: { type: 'string' }, + name: { type: 'string' }, + description: { type: 'string' }, + fields: { type: 'json' }, + views: { type: 'json' }, + }, + }, + }, + metadata: { + type: 'json', + description: 'Operation metadata including total tables count', + }, + }, +} diff --git a/apps/sim/tools/airtable/index.ts b/apps/sim/tools/airtable/index.ts index d2f022c03..2b04f7d54 100644 --- a/apps/sim/tools/airtable/index.ts +++ b/apps/sim/tools/airtable/index.ts @@ -1,12 +1,16 @@ import { airtableCreateRecordsTool } from '@/tools/airtable/create_records' +import { airtableGetBaseSchemaTool } from '@/tools/airtable/get_base_schema' import { airtableGetRecordTool } from '@/tools/airtable/get_record' +import { airtableListBasesTool } from '@/tools/airtable/list_bases' import { airtableListRecordsTool } from '@/tools/airtable/list_records' import { airtableUpdateMultipleRecordsTool } from '@/tools/airtable/update_multiple_records' import { airtableUpdateRecordTool } from '@/tools/airtable/update_record' export { airtableCreateRecordsTool, + airtableGetBaseSchemaTool, airtableGetRecordTool, + airtableListBasesTool, airtableListRecordsTool, airtableUpdateMultipleRecordsTool, airtableUpdateRecordTool, diff --git a/apps/sim/tools/airtable/list_bases.ts b/apps/sim/tools/airtable/list_bases.ts new file mode 100644 index 000000000..0a4ce0c19 --- /dev/null +++ b/apps/sim/tools/airtable/list_bases.ts @@ -0,0 +1,85 @@ +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface AirtableListBasesParams { + accessToken: string +} + +export interface AirtableListBasesResponse extends ToolResponse { + output: { + bases: Array<{ + id: string + name: string + permissionLevel: string + }> + metadata: { + totalBases: number + } + } +} + +export const airtableListBasesTool: ToolConfig = + { + id: 'airtable_list_bases', + name: 'Airtable List Bases', + description: 'List all bases the authenticated user has access to', + version: '1.0.0', + + oauth: { + required: true, + provider: 'airtable', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + }, + + request: { + url: 'https://api.airtable.com/v0/meta/bases', + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + const bases = (data.bases || []).map((base: Record) => ({ + id: base.id, + name: base.name, + permissionLevel: base.permissionLevel, + })) + return { + success: true, + output: { + bases, + metadata: { + totalBases: bases.length, + }, + }, + } + }, + + outputs: { + bases: { + type: 'json', + description: 'Array of Airtable bases with id, name, and permissionLevel', + items: { + type: 'object', + properties: { + id: { type: 'string' }, + name: { type: 'string' }, + permissionLevel: { type: 'string' }, + }, + }, + }, + metadata: { + type: 'json', + description: 'Operation metadata including total bases count', + }, + }, + } diff --git a/apps/sim/tools/airtable/types.ts b/apps/sim/tools/airtable/types.ts index 5c1265855..144fb09fd 100644 --- a/apps/sim/tools/airtable/types.ts +++ b/apps/sim/tools/airtable/types.ts @@ -94,3 +94,25 @@ export type AirtableResponse = | AirtableCreateResponse | AirtableUpdateResponse | AirtableUpdateMultipleResponse + | AirtableListBasesResponse + | AirtableGetBaseSchemaResponse + +export interface AirtableListBasesResponse extends ToolResponse { + output: { + bases: Array<{ id: string; name: string; permissionLevel: string }> + metadata: { totalBases: number } + } +} + +export interface AirtableGetBaseSchemaResponse extends ToolResponse { + output: { + tables: Array<{ + id: string + name: string + description?: string + fields: Array<{ id: string; name: string; type: string; description?: string }> + views: Array<{ id: string; name: string; type: string }> + }> + metadata: { totalTables: number } + } +} diff --git a/apps/sim/tools/knowledge/delete_chunk.ts b/apps/sim/tools/knowledge/delete_chunk.ts new file mode 100644 index 000000000..3bc759af6 --- /dev/null +++ b/apps/sim/tools/knowledge/delete_chunk.ts @@ -0,0 +1,67 @@ +import type { KnowledgeDeleteChunkResponse } from '@/tools/knowledge/types' +import type { ToolConfig } from '@/tools/types' + +export const knowledgeDeleteChunkTool: ToolConfig = { + id: 'knowledge_delete_chunk', + name: 'Knowledge Delete Chunk', + description: 'Delete a chunk from a document in a knowledge base', + version: '1.0.0', + + params: { + knowledgeBaseId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the knowledge base', + }, + documentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the document containing the chunk', + }, + chunkId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the chunk to delete', + }, + }, + + request: { + url: (params) => + `/api/knowledge/${params.knowledgeBaseId}/documents/${params.documentId}/chunks/${params.chunkId}`, + method: 'DELETE', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response, params): Promise => { + const result = await response.json() + + return { + success: true, + output: { + chunkId: params?.chunkId ?? '', + documentId: params?.documentId ?? '', + message: result.data?.message ?? 'Chunk deleted successfully', + }, + } + }, + + outputs: { + chunkId: { + type: 'string', + description: 'ID of the deleted chunk', + }, + documentId: { + type: 'string', + description: 'ID of the parent document', + }, + message: { + type: 'string', + description: 'Confirmation message', + }, + }, +} diff --git a/apps/sim/tools/knowledge/delete_document.ts b/apps/sim/tools/knowledge/delete_document.ts new file mode 100644 index 000000000..39493e382 --- /dev/null +++ b/apps/sim/tools/knowledge/delete_document.ts @@ -0,0 +1,55 @@ +import type { KnowledgeDeleteDocumentResponse } from '@/tools/knowledge/types' +import type { ToolConfig } from '@/tools/types' + +export const knowledgeDeleteDocumentTool: ToolConfig = { + id: 'knowledge_delete_document', + name: 'Knowledge Delete Document', + description: 'Delete a document from a knowledge base', + version: '1.0.0', + + params: { + knowledgeBaseId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the knowledge base containing the document', + }, + documentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the document to delete', + }, + }, + + request: { + url: (params) => `/api/knowledge/${params.knowledgeBaseId}/documents/${params.documentId}`, + method: 'DELETE', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response, params): Promise => { + const result = await response.json() + + return { + success: true, + output: { + documentId: params?.documentId ?? '', + message: result.data?.message ?? 'Document deleted successfully', + }, + } + }, + + outputs: { + documentId: { + type: 'string', + description: 'ID of the deleted document', + }, + message: { + type: 'string', + description: 'Confirmation message', + }, + }, +} diff --git a/apps/sim/tools/knowledge/index.ts b/apps/sim/tools/knowledge/index.ts index 6954b71d9..957825e95 100644 --- a/apps/sim/tools/knowledge/index.ts +++ b/apps/sim/tools/knowledge/index.ts @@ -1,5 +1,21 @@ import { knowledgeCreateDocumentTool } from '@/tools/knowledge/create_document' +import { knowledgeDeleteChunkTool } from '@/tools/knowledge/delete_chunk' +import { knowledgeDeleteDocumentTool } from '@/tools/knowledge/delete_document' +import { knowledgeListChunksTool } from '@/tools/knowledge/list_chunks' +import { knowledgeListDocumentsTool } from '@/tools/knowledge/list_documents' +import { knowledgeListTagsTool } from '@/tools/knowledge/list_tags' import { knowledgeSearchTool } from '@/tools/knowledge/search' +import { knowledgeUpdateChunkTool } from '@/tools/knowledge/update_chunk' import { knowledgeUploadChunkTool } from '@/tools/knowledge/upload_chunk' -export { knowledgeSearchTool, knowledgeUploadChunkTool, knowledgeCreateDocumentTool } +export { + knowledgeSearchTool, + knowledgeUploadChunkTool, + knowledgeCreateDocumentTool, + knowledgeListTagsTool, + knowledgeListDocumentsTool, + knowledgeDeleteDocumentTool, + knowledgeListChunksTool, + knowledgeUpdateChunkTool, + knowledgeDeleteChunkTool, +} diff --git a/apps/sim/tools/knowledge/list_chunks.ts b/apps/sim/tools/knowledge/list_chunks.ts new file mode 100644 index 000000000..7d7f559a6 --- /dev/null +++ b/apps/sim/tools/knowledge/list_chunks.ts @@ -0,0 +1,144 @@ +import type { KnowledgeListChunksResponse } from '@/tools/knowledge/types' +import type { ToolConfig } from '@/tools/types' + +export const knowledgeListChunksTool: ToolConfig = { + id: 'knowledge_list_chunks', + name: 'Knowledge List Chunks', + description: + 'List chunks for a document in a knowledge base with optional filtering and pagination', + version: '1.0.0', + + params: { + knowledgeBaseId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the knowledge base', + }, + documentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the document to list chunks from', + }, + search: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search query to filter chunks by content', + }, + enabled: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by enabled status: "true", "false", or "all" (default: "all")', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of chunks to return (1-100, default: 50)', + }, + offset: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of chunks to skip for pagination (default: 0)', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams() + if (params.search) queryParams.set('search', params.search) + if (params.enabled) queryParams.set('enabled', params.enabled) + if (params.limit) + queryParams.set('limit', String(Math.max(1, Math.min(100, Number(params.limit))))) + if (params.offset) queryParams.set('offset', String(params.offset)) + const qs = queryParams.toString() + return `/api/knowledge/${params.knowledgeBaseId}/documents/${params.documentId}/chunks${qs ? `?${qs}` : ''}` + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response, params): Promise => { + const result = await response.json() + const chunks = result.data || [] + const pagination = result.pagination || {} + + return { + success: true, + output: { + knowledgeBaseId: params?.knowledgeBaseId ?? '', + documentId: params?.documentId ?? '', + chunks: chunks.map( + (chunk: { + id: string + chunkIndex: number + content: string + contentLength: number + tokenCount: number + enabled: boolean + createdAt: string + updatedAt: string + }) => ({ + id: chunk.id, + chunkIndex: chunk.chunkIndex ?? 0, + content: chunk.content, + contentLength: chunk.contentLength ?? 0, + tokenCount: chunk.tokenCount ?? 0, + enabled: chunk.enabled ?? true, + createdAt: chunk.createdAt ?? null, + updatedAt: chunk.updatedAt ?? null, + }) + ), + totalChunks: pagination.total ?? chunks.length, + limit: pagination.limit ?? 50, + offset: pagination.offset ?? 0, + }, + } + }, + + outputs: { + knowledgeBaseId: { + type: 'string', + description: 'ID of the knowledge base', + }, + documentId: { + type: 'string', + description: 'ID of the document', + }, + chunks: { + type: 'array', + description: 'Array of chunks in the document', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Chunk ID' }, + chunkIndex: { type: 'number', description: 'Index of the chunk within the document' }, + content: { type: 'string', description: 'Chunk text content' }, + contentLength: { type: 'number', description: 'Content length in characters' }, + tokenCount: { type: 'number', description: 'Token count for the chunk' }, + enabled: { type: 'boolean', description: 'Whether the chunk is enabled' }, + createdAt: { type: 'string', description: 'Creation timestamp' }, + updatedAt: { type: 'string', description: 'Last update timestamp' }, + }, + }, + }, + totalChunks: { + type: 'number', + description: 'Total number of chunks matching the filter', + }, + limit: { + type: 'number', + description: 'Page size used', + }, + offset: { + type: 'number', + description: 'Offset used for pagination', + }, + }, +} diff --git a/apps/sim/tools/knowledge/list_documents.ts b/apps/sim/tools/knowledge/list_documents.ts new file mode 100644 index 000000000..4ad8b7dc5 --- /dev/null +++ b/apps/sim/tools/knowledge/list_documents.ts @@ -0,0 +1,141 @@ +import type { KnowledgeListDocumentsResponse } from '@/tools/knowledge/types' +import type { ToolConfig } from '@/tools/types' + +export const knowledgeListDocumentsTool: ToolConfig = { + id: 'knowledge_list_documents', + name: 'Knowledge List Documents', + description: 'List documents in a knowledge base with optional filtering, search, and pagination', + version: '1.0.0', + + params: { + knowledgeBaseId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the knowledge base to list documents from', + }, + search: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search query to filter documents by filename', + }, + enabledFilter: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by enabled status: "all", "enabled", or "disabled"', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of documents to return (default: 50)', + }, + offset: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of documents to skip for pagination (default: 0)', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams() + if (params.search) queryParams.set('search', params.search) + if (params.enabledFilter) queryParams.set('enabledFilter', params.enabledFilter) + if (params.limit) queryParams.set('limit', String(params.limit)) + if (params.offset) queryParams.set('offset', String(params.offset)) + const qs = queryParams.toString() + return `/api/knowledge/${params.knowledgeBaseId}/documents${qs ? `?${qs}` : ''}` + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response, params): Promise => { + const result = await response.json() + const data = result.data || {} + const documents = data.documents || [] + const pagination = data.pagination || {} + + return { + success: true, + output: { + knowledgeBaseId: params?.knowledgeBaseId ?? '', + documents: documents.map( + (doc: { + id: string + filename: string + fileSize: number + mimeType: string + enabled: boolean + processingStatus: string + chunkCount: number + tokenCount: number + uploadedAt: string + updatedAt: string + }) => ({ + id: doc.id, + filename: doc.filename, + fileSize: doc.fileSize ?? 0, + mimeType: doc.mimeType ?? null, + enabled: doc.enabled ?? true, + processingStatus: doc.processingStatus ?? null, + chunkCount: doc.chunkCount ?? 0, + tokenCount: doc.tokenCount ?? 0, + uploadedAt: doc.uploadedAt ?? null, + updatedAt: doc.updatedAt ?? null, + }) + ), + totalDocuments: pagination.total ?? documents.length, + limit: pagination.limit ?? 50, + offset: pagination.offset ?? 0, + }, + } + }, + + outputs: { + knowledgeBaseId: { + type: 'string', + description: 'ID of the knowledge base', + }, + documents: { + type: 'array', + description: 'Array of documents in the knowledge base', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Document ID' }, + filename: { type: 'string', description: 'Document filename' }, + fileSize: { type: 'number', description: 'File size in bytes' }, + mimeType: { type: 'string', description: 'MIME type of the document' }, + enabled: { type: 'boolean', description: 'Whether the document is enabled' }, + processingStatus: { + type: 'string', + description: 'Processing status (pending, processing, completed, failed)', + }, + chunkCount: { type: 'number', description: 'Number of chunks in the document' }, + tokenCount: { type: 'number', description: 'Total token count across chunks' }, + uploadedAt: { type: 'string', description: 'Upload timestamp' }, + updatedAt: { type: 'string', description: 'Last update timestamp' }, + }, + }, + }, + totalDocuments: { + type: 'number', + description: 'Total number of documents matching the filter', + }, + limit: { + type: 'number', + description: 'Page size used', + }, + offset: { + type: 'number', + description: 'Offset used for pagination', + }, + }, +} diff --git a/apps/sim/tools/knowledge/list_tags.ts b/apps/sim/tools/knowledge/list_tags.ts new file mode 100644 index 000000000..fbe95a6a2 --- /dev/null +++ b/apps/sim/tools/knowledge/list_tags.ts @@ -0,0 +1,85 @@ +import type { KnowledgeListTagsResponse } from '@/tools/knowledge/types' +import type { ToolConfig } from '@/tools/types' + +export const knowledgeListTagsTool: ToolConfig = { + id: 'knowledge_list_tags', + name: 'Knowledge List Tags', + description: 'List all tag definitions for a knowledge base', + version: '1.0.0', + + params: { + knowledgeBaseId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the knowledge base to list tags for', + }, + }, + + request: { + url: (params) => `/api/knowledge/${params.knowledgeBaseId}/tag-definitions`, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response, params): Promise => { + const result = await response.json() + const tags = result.data || [] + + return { + success: true, + output: { + knowledgeBaseId: params?.knowledgeBaseId ?? '', + tags: tags.map( + (tag: { + id: string + tagSlot: string + displayName: string + fieldType: string + createdAt: string + updatedAt: string + }) => ({ + id: tag.id, + tagSlot: tag.tagSlot, + displayName: tag.displayName, + fieldType: tag.fieldType, + createdAt: tag.createdAt ?? null, + updatedAt: tag.updatedAt ?? null, + }) + ), + totalTags: tags.length, + }, + } + }, + + outputs: { + knowledgeBaseId: { + type: 'string', + description: 'ID of the knowledge base', + }, + tags: { + type: 'array', + description: 'Array of tag definitions for the knowledge base', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Tag definition ID' }, + tagSlot: { type: 'string', description: 'Internal tag slot (e.g. tag1, number1)' }, + displayName: { type: 'string', description: 'Human-readable tag name' }, + fieldType: { + type: 'string', + description: 'Tag field type (text, number, date, boolean)', + }, + createdAt: { type: 'string', description: 'Creation timestamp' }, + updatedAt: { type: 'string', description: 'Last update timestamp' }, + }, + }, + }, + totalTags: { + type: 'number', + description: 'Total number of tag definitions', + }, + }, +} diff --git a/apps/sim/tools/knowledge/types.ts b/apps/sim/tools/knowledge/types.ts index 2d632e3cc..6e5de67f9 100644 --- a/apps/sim/tools/knowledge/types.ts +++ b/apps/sim/tools/knowledge/types.ts @@ -100,3 +100,109 @@ export interface KnowledgeCreateDocumentResponse { } error?: string } + +export interface KnowledgeTagDefinition { + id: string + tagSlot: string + displayName: string + fieldType: string + createdAt: string | null + updatedAt: string | null +} + +export interface KnowledgeListTagsParams { + knowledgeBaseId: string +} + +export interface KnowledgeListTagsResponse { + success: boolean + output: { + knowledgeBaseId: string + tags: KnowledgeTagDefinition[] + totalTags: number + } + error?: string +} + +export interface KnowledgeDocumentSummary { + id: string + filename: string + fileSize: number + mimeType: string | null + enabled: boolean + processingStatus: string | null + chunkCount: number + tokenCount: number + uploadedAt: string | null + updatedAt: string | null +} + +export interface KnowledgeListDocumentsResponse { + success: boolean + output: { + knowledgeBaseId: string + documents: KnowledgeDocumentSummary[] + totalDocuments: number + limit: number + offset: number + } + error?: string +} + +export interface KnowledgeDeleteDocumentResponse { + success: boolean + output: { + documentId: string + message: string + } + error?: string +} + +export interface KnowledgeChunkSummary { + id: string + chunkIndex: number + content: string + contentLength: number + tokenCount: number + enabled: boolean + createdAt: string | null + updatedAt: string | null +} + +export interface KnowledgeListChunksResponse { + success: boolean + output: { + knowledgeBaseId: string + documentId: string + chunks: KnowledgeChunkSummary[] + totalChunks: number + limit: number + offset: number + } + error?: string +} + +export interface KnowledgeUpdateChunkResponse { + success: boolean + output: { + documentId: string + id: string + chunkIndex: number + content: string + contentLength: number + tokenCount: number + enabled: boolean + updatedAt: string | null + } + error?: string +} + +export interface KnowledgeDeleteChunkResponse { + success: boolean + output: { + chunkId: string + documentId: string + message: string + } + error?: string +} diff --git a/apps/sim/tools/knowledge/update_chunk.ts b/apps/sim/tools/knowledge/update_chunk.ts new file mode 100644 index 000000000..1ddaa4121 --- /dev/null +++ b/apps/sim/tools/knowledge/update_chunk.ts @@ -0,0 +1,112 @@ +import type { KnowledgeUpdateChunkResponse } from '@/tools/knowledge/types' +import type { ToolConfig } from '@/tools/types' + +export const knowledgeUpdateChunkTool: ToolConfig = { + id: 'knowledge_update_chunk', + name: 'Knowledge Update Chunk', + description: 'Update the content or enabled status of a chunk in a knowledge base', + version: '1.0.0', + + params: { + knowledgeBaseId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the knowledge base', + }, + documentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the document containing the chunk', + }, + chunkId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the chunk to update', + }, + content: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New content for the chunk', + }, + enabled: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the chunk should be enabled or disabled', + }, + }, + + request: { + url: (params) => + `/api/knowledge/${params.knowledgeBaseId}/documents/${params.documentId}/chunks/${params.chunkId}`, + method: 'PUT', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = {} + if (params.content !== undefined) body.content = params.content + if (params.enabled !== undefined) body.enabled = params.enabled + return body + }, + }, + + transformResponse: async (response, params): Promise => { + const result = await response.json() + const chunk = result.data || {} + + return { + success: true, + output: { + documentId: params?.documentId ?? '', + id: chunk.id ?? '', + chunkIndex: chunk.chunkIndex ?? 0, + content: chunk.content ?? '', + contentLength: chunk.contentLength ?? 0, + tokenCount: chunk.tokenCount ?? 0, + enabled: chunk.enabled ?? true, + updatedAt: chunk.updatedAt ?? null, + }, + } + }, + + outputs: { + documentId: { + type: 'string', + description: 'ID of the parent document', + }, + id: { + type: 'string', + description: 'Chunk ID', + }, + chunkIndex: { + type: 'number', + description: 'Index of the chunk within the document', + }, + content: { + type: 'string', + description: 'Updated chunk content', + }, + contentLength: { + type: 'number', + description: 'Content length in characters', + }, + tokenCount: { + type: 'number', + description: 'Token count for the chunk', + }, + enabled: { + type: 'boolean', + description: 'Whether the chunk is enabled', + }, + updatedAt: { + type: 'string', + description: 'Last update timestamp', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 991c7e140..ba44898a4 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -20,8 +20,11 @@ import { } from '@/tools/ahrefs' import { airtableCreateRecordsTool, + airtableGetBaseSchemaTool, airtableGetRecordTool, + airtableListBasesTool, airtableListRecordsTool, + airtableUpdateMultipleRecordsTool, airtableUpdateRecordTool, } from '@/tools/airtable' import { airweaveSearchTool } from '@/tools/airweave' @@ -899,7 +902,13 @@ import { } from '@/tools/kalshi' import { knowledgeCreateDocumentTool, + knowledgeDeleteChunkTool, + knowledgeDeleteDocumentTool, + knowledgeListChunksTool, + knowledgeListDocumentsTool, + knowledgeListTagsTool, knowledgeSearchTool, + knowledgeUpdateChunkTool, knowledgeUploadChunkTool, } from '@/tools/knowledge' import { langsmithCreateRunsBatchTool, langsmithCreateRunTool } from '@/tools/langsmith' @@ -2705,8 +2714,11 @@ export const tools: Record = { twilio_voice_list_calls: listCallsTool, twilio_voice_get_recording: getRecordingTool, airtable_create_records: airtableCreateRecordsTool, + airtable_get_base_schema: airtableGetBaseSchemaTool, airtable_get_record: airtableGetRecordTool, + airtable_list_bases: airtableListBasesTool, airtable_list_records: airtableListRecordsTool, + airtable_update_multiple_records: airtableUpdateMultipleRecordsTool, airtable_update_record: airtableUpdateRecordTool, ahrefs_domain_rating: ahrefsDomainRatingTool, ahrefs_backlinks: ahrefsBacklinksTool, @@ -2774,6 +2786,12 @@ export const tools: Record = { knowledge_search: knowledgeSearchTool, knowledge_upload_chunk: knowledgeUploadChunkTool, knowledge_create_document: knowledgeCreateDocumentTool, + knowledge_list_tags: knowledgeListTagsTool, + knowledge_list_documents: knowledgeListDocumentsTool, + knowledge_delete_document: knowledgeDeleteDocumentTool, + knowledge_list_chunks: knowledgeListChunksTool, + knowledge_update_chunk: knowledgeUpdateChunkTool, + knowledge_delete_chunk: knowledgeDeleteChunkTool, search_tool: searchTool, elevenlabs_tts: elevenLabsTtsTool, stt_whisper: whisperSttTool, diff --git a/packages/db/migrations/0155_talented_ben_parker.sql b/packages/db/migrations/0155_talented_ben_parker.sql new file mode 100644 index 000000000..9ed3cf740 --- /dev/null +++ b/packages/db/migrations/0155_talented_ben_parker.sql @@ -0,0 +1,45 @@ +CREATE TABLE "knowledge_connector" ( + "id" text PRIMARY KEY NOT NULL, + "knowledge_base_id" text NOT NULL, + "connector_type" text NOT NULL, + "credential_id" text NOT NULL, + "source_config" json NOT NULL, + "sync_mode" text DEFAULT 'incremental' NOT NULL, + "sync_interval_minutes" integer DEFAULT 1440 NOT NULL, + "status" text DEFAULT 'active' NOT NULL, + "last_sync_at" timestamp, + "last_sync_error" text, + "last_sync_doc_count" integer, + "next_sync_at" timestamp, + "consecutive_failures" integer DEFAULT 0 NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + "deleted_at" timestamp +); +--> statement-breakpoint +CREATE TABLE "knowledge_connector_sync_log" ( + "id" text PRIMARY KEY NOT NULL, + "connector_id" text NOT NULL, + "status" text NOT NULL, + "started_at" timestamp DEFAULT now() NOT NULL, + "completed_at" timestamp, + "docs_added" integer DEFAULT 0 NOT NULL, + "docs_updated" integer DEFAULT 0 NOT NULL, + "docs_deleted" integer DEFAULT 0 NOT NULL, + "docs_unchanged" integer DEFAULT 0 NOT NULL, + "error_message" text +); +--> statement-breakpoint +ALTER TABLE "document" ADD COLUMN "user_excluded" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "document" ADD COLUMN "connector_id" text;--> statement-breakpoint +ALTER TABLE "document" ADD COLUMN "external_id" text;--> statement-breakpoint +ALTER TABLE "document" ADD COLUMN "content_hash" text;--> statement-breakpoint +ALTER TABLE "document" ADD COLUMN "source_url" text;--> statement-breakpoint +ALTER TABLE "knowledge_connector" ADD CONSTRAINT "knowledge_connector_knowledge_base_id_knowledge_base_id_fk" FOREIGN KEY ("knowledge_base_id") REFERENCES "public"."knowledge_base"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "knowledge_connector_sync_log" ADD CONSTRAINT "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk" FOREIGN KEY ("connector_id") REFERENCES "public"."knowledge_connector"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "kc_knowledge_base_id_idx" ON "knowledge_connector" USING btree ("knowledge_base_id");--> statement-breakpoint +CREATE INDEX "kc_status_next_sync_idx" ON "knowledge_connector" USING btree ("status","next_sync_at");--> statement-breakpoint +CREATE INDEX "kcsl_connector_id_idx" ON "knowledge_connector_sync_log" USING btree ("connector_id");--> statement-breakpoint +ALTER TABLE "document" ADD CONSTRAINT "document_connector_id_knowledge_connector_id_fk" FOREIGN KEY ("connector_id") REFERENCES "public"."knowledge_connector"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "doc_connector_external_id_idx" ON "document" USING btree ("connector_id","external_id") WHERE "document"."deleted_at" IS NULL;--> statement-breakpoint +CREATE INDEX "doc_connector_id_idx" ON "document" USING btree ("connector_id"); \ No newline at end of file diff --git a/packages/db/migrations/meta/0155_snapshot.json b/packages/db/migrations/meta/0155_snapshot.json new file mode 100644 index 000000000..05614038a --- /dev/null +++ b/packages/db/migrations/meta/0155_snapshot.json @@ -0,0 +1,11300 @@ +{ + "id": "4209f841-3ebf-469f-ad98-2df8a84766a1", + "prevId": "49f580f7-7eba-4431-bdf4-61db0e606546", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.a2a_agent": { + "name": "a2a_agent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "authentication": { + "name": "authentication", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "signatures": { + "name": "signatures", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_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": { + "a2a_agent_workspace_id_idx": { + "name": "a2a_agent_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workflow_id_idx": { + "name": "a2a_agent_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_created_by_idx": { + "name": "a2a_agent_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_workflow_unique": { + "name": "a2a_agent_workspace_workflow_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_agent_workspace_id_workspace_id_fk": { + "name": "a2a_agent_workspace_id_workspace_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_workflow_id_workflow_id_fk": { + "name": "a2a_agent_workflow_id_workflow_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_created_by_user_id_fk": { + "name": "a2a_agent_created_by_user_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_push_notification_config": { + "name": "a2a_push_notification_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_schemes": { + "name": "auth_schemes", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "auth_credentials": { + "name": "auth_credentials", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "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": { + "a2a_push_notification_config_task_id_idx": { + "name": "a2a_push_notification_config_task_id_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_push_notification_config_task_unique": { + "name": "a2a_push_notification_config_task_unique", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_push_notification_config_task_id_a2a_task_id_fk": { + "name": "a2a_push_notification_config_task_id_a2a_task_id_fk", + "tableFrom": "a2a_push_notification_config", + "tableTo": "a2a_task", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_task": { + "name": "a2a_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "a2a_task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'submitted'" + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "artifacts": { + "name": "artifacts", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "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()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "a2a_task_agent_id_idx": { + "name": "a2a_task_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_session_id_idx": { + "name": "a2a_task_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_status_idx": { + "name": "a2a_task_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_execution_id_idx": { + "name": "a2a_task_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_created_at_idx": { + "name": "a2a_task_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_task_agent_id_a2a_agent_id_fk": { + "name": "a2a_task_agent_id_a2a_agent_id_fk", + "tableFrom": "a2a_task", + "tableTo": "a2a_agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "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 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_user_provider_unique": { + "name": "account_user_provider_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "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()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.async_jobs": { + "name": "async_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "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 + }, + "run_at": { + "name": "run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "async_jobs_status_started_at_idx": { + "name": "async_jobs_status_started_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_status_completed_at_idx": { + "name": "async_jobs_status_completed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "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": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "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 + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_artifact": { + "name": "plan_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "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": { + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "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": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set": { + "name": "credential_set", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "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 + }, + "provider_id": { + "name": "provider_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, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_organization_id_idx": { + "name": "credential_set_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_created_by_idx": { + "name": "credential_set_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_org_name_unique": { + "name": "credential_set_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_provider_id_idx": { + "name": "credential_set_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_organization_id_organization_id_fk": { + "name": "credential_set_organization_id_organization_id_fk", + "tableFrom": "credential_set", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_created_by_user_id_fk": { + "name": "credential_set_created_by_user_id_fk", + "tableFrom": "credential_set", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_invitation": { + "name": "credential_set_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "accepted_by_user_id": { + "name": "accepted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_invitation_set_id_idx": { + "name": "credential_set_invitation_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_token_idx": { + "name": "credential_set_invitation_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_status_idx": { + "name": "credential_set_invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_expires_at_idx": { + "name": "credential_set_invitation_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_invitation_credential_set_id_credential_set_id_fk": { + "name": "credential_set_invitation_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_invited_by_user_id_fk": { + "name": "credential_set_invitation_invited_by_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_accepted_by_user_id_user_id_fk": { + "name": "credential_set_invitation_accepted_by_user_id_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["accepted_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credential_set_invitation_token_unique": { + "name": "credential_set_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_member": { + "name": "credential_set_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "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": { + "credential_set_member_set_id_idx": { + "name": "credential_set_member_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_user_id_idx": { + "name": "credential_set_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_unique": { + "name": "credential_set_member_unique", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_status_idx": { + "name": "credential_set_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_member_credential_set_id_credential_set_id_fk": { + "name": "credential_set_member_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_user_id_user_id_fk": { + "name": "credential_set_member_user_id_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_invited_by_user_id_fk": { + "name": "credential_set_member_invited_by_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "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": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "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": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_excluded": { + "name": "user_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_external_id_idx": { + "name": "doc_connector_external_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_id_idx": { + "name": "doc_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_connector_id_knowledge_connector_id_fk": { + "name": "document_connector_id_knowledge_connector_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "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": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.form": { + "name": "form", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "show_branding": { + "name": "show_branding", + "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": { + "form_identifier_idx": { + "name": "form_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_workflow_id_idx": { + "name": "form_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_user_id_idx": { + "name": "form_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "form_workflow_id_workflow_id_fk": { + "name": "form_workflow_id_workflow_id_fk", + "tableFrom": "form", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "form_user_id_user_id_fk": { + "name": "form_user_id_user_id_fk", + "tableFrom": "form", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "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()" + } + }, + "indexes": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "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" + }, + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_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": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "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_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector": { + "name": "knowledge_connector", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_config": { + "name": "source_config", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "sync_mode": { + "name": "sync_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'incremental'" + }, + "sync_interval_minutes": { + "name": "sync_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1440 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync_doc_count": { + "name": "last_sync_doc_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "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()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kc_knowledge_base_id_idx": { + "name": "kc_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_status_next_sync_idx": { + "name": "kc_status_next_sync_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector_sync_log": { + "name": "knowledge_connector_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_deleted": { + "name": "docs_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_id_idx": { + "name": "kcsl_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_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": { + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_idx": { + "name": "mcp_servers_workspace_deleted_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": ["created_by"], + "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 + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_user_id_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "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_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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "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": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_idx": { + "name": "memory_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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 + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "departed_member_usage": { + "name": "departed_member_usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "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": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "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 + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "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()" + }, + "auto_add_new_members": { + "name": "auto_add_new_members", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "permission_group_organization_id_idx": { + "name": "permission_group_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_org_name_unique": { + "name": "permission_group_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_org_auto_add_unique": { + "name": "permission_group_org_auto_add_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "auto_add_new_members = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_organization_id_organization_id_fk": { + "name": "permission_group_organization_id_organization_id_fk", + "tableFrom": "permission_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_member_group_id_idx": { + "name": "permission_group_member_group_id_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_user_id_unique": { + "name": "permission_group_member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "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": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_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": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_idx": { + "name": "permissions_user_entity_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "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": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.referral_attribution": { + "name": "referral_attribution", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "campaign_id": { + "name": "campaign_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_source": { + "name": "utm_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_medium": { + "name": "utm_medium", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_campaign": { + "name": "utm_campaign", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_content": { + "name": "utm_content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "referrer_url": { + "name": "referrer_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "landing_page": { + "name": "landing_page", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bonus_credit_amount": { + "name": "bonus_credit_amount", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "referral_attribution_user_id_idx": { + "name": "referral_attribution_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "referral_attribution_org_unique_idx": { + "name": "referral_attribution_org_unique_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"referral_attribution\".\"organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "referral_attribution_campaign_id_idx": { + "name": "referral_attribution_campaign_id_idx", + "columns": [ + { + "expression": "campaign_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "referral_attribution_utm_campaign_idx": { + "name": "referral_attribution_utm_campaign_idx", + "columns": [ + { + "expression": "utm_campaign", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "referral_attribution_utm_content_idx": { + "name": "referral_attribution_utm_content_idx", + "columns": [ + { + "expression": "utm_content", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "referral_attribution_created_at_idx": { + "name": "referral_attribution_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "referral_attribution_user_id_user_id_fk": { + "name": "referral_attribution_user_id_user_id_fk", + "tableFrom": "referral_attribution", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "referral_attribution_organization_id_organization_id_fk": { + "name": "referral_attribution_organization_id_organization_id_fk", + "tableFrom": "referral_attribution", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "referral_attribution_campaign_id_referral_campaigns_id_fk": { + "name": "referral_attribution_campaign_id_referral_campaigns_id_fk", + "tableFrom": "referral_attribution", + "tableTo": "referral_campaigns", + "columnsFrom": ["campaign_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "referral_attribution_user_id_unique": { + "name": "referral_attribution_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.referral_campaigns": { + "name": "referral_campaigns", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_source": { + "name": "utm_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_medium": { + "name": "utm_medium", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_campaign": { + "name": "utm_campaign", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_content": { + "name": "utm_content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bonus_credit_amount": { + "name": "bonus_credit_amount", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "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": { + "referral_campaigns_active_idx": { + "name": "referral_campaigns_active_idx", + "columns": [ + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "referral_campaigns_code_unique": { + "name": "referral_campaigns_code_unique", + "nullsNotDistinct": false, + "columns": ["code"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "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 + }, + "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 + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_token_idx": { + "name": "session_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'dark'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "copilot_enabled_models": { + "name": "copilot_enabled_models", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill": { + "name": "skill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "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": { + "skill_workspace_id_idx": { + "name": "skill_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "skill_workspace_name_unique": { + "name": "skill_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_workspace_id_workspace_id_fk": { + "name": "skill_workspace_id_workspace_id_fk", + "tableFrom": "skill", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_user_id_user_id_fk": { + "name": "skill_user_id_user_id_fk", + "tableFrom": "skill", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "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 + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sso_provider_provider_id_idx": { + "name": "sso_provider_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.template_creators": { + "name": "template_creators", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "reference_type": { + "name": "reference_type", + "type": "template_creator_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile_image_url": { + "name": "profile_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "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": { + "template_creators_reference_idx": { + "name": "template_creators_reference_idx", + "columns": [ + { + "expression": "reference_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_reference_id_idx": { + "name": "template_creators_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_created_by_idx": { + "name": "template_creators_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_creators_created_by_user_id_fk": { + "name": "template_creators_created_by_user_id_fk", + "tableFrom": "template_creators", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.template_stars": { + "name": "template_stars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "starred_at": { + "name": "starred_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_stars_user_id_idx": { + "name": "template_stars_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_id_idx": { + "name": "template_stars_template_id_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_idx": { + "name": "template_stars_user_template_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_user_idx": { + "name": "template_stars_template_user_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_starred_at_idx": { + "name": "template_stars_starred_at_idx", + "columns": [ + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_starred_at_idx": { + "name": "template_stars_template_starred_at_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_unique": { + "name": "template_stars_user_template_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_stars_user_id_user_id_fk": { + "name": "template_stars_user_id_user_id_fk", + "tableFrom": "template_stars", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "template_stars_template_id_templates_id_fk": { + "name": "template_stars_template_id_templates_id_fk", + "tableFrom": "template_stars", + "tableTo": "templates", + "columnsFrom": ["template_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.templates": { + "name": "templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "stars": { + "name": "stars", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "template_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "required_credentials": { + "name": "required_credentials", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "og_image_url": { + "name": "og_image_url", + "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": { + "templates_status_idx": { + "name": "templates_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_creator_id_idx": { + "name": "templates_creator_id_idx", + "columns": [ + { + "expression": "creator_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_views_idx": { + "name": "templates_views_idx", + "columns": [ + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_stars_idx": { + "name": "templates_stars_idx", + "columns": [ + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_views_idx": { + "name": "templates_status_views_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_stars_idx": { + "name": "templates_status_stars_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_created_at_idx": { + "name": "templates_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_updated_at_idx": { + "name": "templates_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "templates_workflow_id_workflow_id_fk": { + "name": "templates_workflow_id_workflow_id_fk", + "tableFrom": "templates", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "templates_creator_id_template_creators_id_fk": { + "name": "templates_creator_id_template_creators_id_fk", + "tableFrom": "templates", + "tableTo": "template_creators", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_id_idx": { + "name": "usage_log_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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 + }, + "image": { + "name": "image", + "type": "text", + "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 + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_super_user": { + "name": "is_super_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_executions": { + "name": "total_mcp_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_a2a_executions": { + "name": "total_a2a_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'20'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "pro_period_cost_snapshot": { + "name": "pro_period_cost_snapshot", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_copilot_cost": { + "name": "current_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_copilot_tokens": { + "name": "total_copilot_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_calls": { + "name": "total_mcp_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_cost": { + "name": "total_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_mcp_copilot_cost": { + "name": "current_period_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "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": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "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": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credential_set_id": { + "name": "credential_set_id", + "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": { + "path_deployment_unique": { + "name": "path_deployment_unique", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id": { + "name": "idx_webhook_on_workflow_id_block_id", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_workflow_deployment_idx": { + "name": "webhook_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_credential_set_id_idx": { + "name": "webhook_credential_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_credential_set_id_credential_set_id_fk": { + "name": "webhook_credential_set_id_credential_set_id_fk", + "tableFrom": "webhook", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#3972F6'" + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "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 + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_workflow_folder_id_fk": { + "name": "workflow_folder_id_workflow_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "workflow_folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "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": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_checkpoints": { + "name": "workflow_checkpoints", + "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 + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "type": "json", + "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": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "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": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "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": { + "workflow_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "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": { + "workflow_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": ["server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_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": { + "workflow_schedule_workflow_block_deployment_unique": { + "name": "workflow_schedule_workflow_block_deployment_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_workflow_deployment_idx": { + "name": "workflow_schedule_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "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": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "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": {}, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "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": { + "workspace_byok_provider_unique": { + "name": "workspace_byok_provider_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_byok_workspace_idx": { + "name": "workspace_byok_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "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": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_key_idx": { + "name": "workspace_file_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_files_key_unique": { + "name": "workspace_files_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_invitation": { + "name": "workspace_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "workspace_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + }, + "org_invitation_id": { + "name": "org_invitation_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_invitation_workspace_id_workspace_id_fk": { + "name": "workspace_invitation_workspace_id_workspace_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_invitation_inviter_id_user_id_fk": { + "name": "workspace_invitation_inviter_id_user_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_invitation_token_unique": { + "name": "workspace_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_notification_delivery": { + "name": "workspace_notification_delivery", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "notification_delivery_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "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": { + "workspace_notification_delivery_subscription_id_idx": { + "name": "workspace_notification_delivery_subscription_id_idx", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_execution_id_idx": { + "name": "workspace_notification_delivery_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_status_idx": { + "name": "workspace_notification_delivery_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_next_attempt_idx": { + "name": "workspace_notification_delivery_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk": { + "name": "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk", + "tableFrom": "workspace_notification_delivery", + "tableTo": "workspace_notification_subscription", + "columnsFrom": ["subscription_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_notification_delivery_workflow_id_workflow_id_fk": { + "name": "workspace_notification_delivery_workflow_id_workflow_id_fk", + "tableFrom": "workspace_notification_delivery", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_notification_subscription": { + "name": "workspace_notification_subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notification_type": { + "name": "notification_type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workflow_ids": { + "name": "workflow_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "all_workflows": { + "name": "all_workflows", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "level_filter": { + "name": "level_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['info', 'error']::text[]" + }, + "trigger_filter": { + "name": "trigger_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['api', 'webhook', 'schedule', 'manual', 'chat']::text[]" + }, + "include_final_output": { + "name": "include_final_output", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_trace_spans": { + "name": "include_trace_spans", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_rate_limits": { + "name": "include_rate_limits", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_usage_data": { + "name": "include_usage_data", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "webhook_config": { + "name": "webhook_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "email_recipients": { + "name": "email_recipients", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "slack_config": { + "name": "slack_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "alert_config": { + "name": "alert_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_alert_at": { + "name": "last_alert_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "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": { + "workspace_notification_workspace_id_idx": { + "name": "workspace_notification_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_active_idx": { + "name": "workspace_notification_active_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_type_idx": { + "name": "workspace_notification_type_idx", + "columns": [ + { + "expression": "notification_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_notification_subscription_workspace_id_workspace_id_fk": { + "name": "workspace_notification_subscription_workspace_id_workspace_id_fk", + "tableFrom": "workspace_notification_subscription", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_notification_subscription_created_by_user_id_fk": { + "name": "workspace_notification_subscription_created_by_user_id_fk", + "tableFrom": "workspace_notification_subscription", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.a2a_task_status": { + "name": "a2a_task_status", + "schema": "public", + "values": [ + "submitted", + "working", + "input-required", + "completed", + "failed", + "canceled", + "rejected", + "auth-required", + "unknown" + ] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.credential_set_invitation_status": { + "name": "credential_set_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "expired", "cancelled"] + }, + "public.credential_set_member_status": { + "name": "credential_set_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.notification_delivery_status": { + "name": "notification_delivery_status", + "schema": "public", + "values": ["pending", "in_progress", "success", "failed"] + }, + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": ["webhook", "email", "slack"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.template_creator_type": { + "name": "template_creator_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.template_status": { + "name": "template_status", + "schema": "public", + "values": ["pending", "approved", "rejected"] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": ["workflow", "wand", "copilot", "mcp_copilot"] + }, + "public.workspace_invitation_status": { + "name": "workspace_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 2b83d1c90..d4a736939 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1079,6 +1079,13 @@ "when": 1770869658697, "tag": "0154_bumpy_living_mummy", "breakpoints": true + }, + { + "idx": 155, + "version": "7", + "when": 1771305981173, + "tag": "0155_talented_ben_parker", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 090ab0855..03cd051eb 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -1215,6 +1215,7 @@ export const document = pgTable( // Document state enabled: boolean('enabled').notNull().default(true), // Enable/disable from knowledge base deletedAt: timestamp('deleted_at'), // Soft delete + userExcluded: boolean('user_excluded').notNull().default(false), // User explicitly excluded — skip on sync // Document tags for filtering (inherited by all chunks) // Text tags (7 slots) @@ -1239,6 +1240,14 @@ export const document = pgTable( boolean2: boolean('boolean2'), boolean3: boolean('boolean3'), + // Connector-sourced document fields + connectorId: text('connector_id').references(() => knowledgeConnector.id, { + onDelete: 'set null', + }), + externalId: text('external_id'), + contentHash: text('content_hash'), + sourceUrl: text('source_url'), + // Timestamps uploadedAt: timestamp('uploaded_at').notNull().defaultNow(), }, @@ -1252,6 +1261,12 @@ export const document = pgTable( table.knowledgeBaseId, table.processingStatus ), + // Connector document uniqueness (partial — only non-deleted rows) + connectorExternalIdIdx: uniqueIndex('doc_connector_external_id_idx') + .on(table.connectorId, table.externalId) + .where(sql`${table.deletedAt} IS NULL`), + // Sync engine: load all active docs for a connector + connectorIdIdx: index('doc_connector_id_idx').on(table.connectorId), // Text tag indexes tag1Idx: index('doc_tag1_idx').on(table.tag1), tag2Idx: index('doc_tag2_idx').on(table.tag2), @@ -2240,3 +2255,60 @@ export const asyncJobs = pgTable( ), }) ) + +/** + * Knowledge Connector - persistent link to an external source (Confluence, Google Drive, etc.) + * that syncs documents into a knowledge base. + */ +export const knowledgeConnector = pgTable( + 'knowledge_connector', + { + id: text('id').primaryKey(), + knowledgeBaseId: text('knowledge_base_id') + .notNull() + .references(() => knowledgeBase.id, { onDelete: 'cascade' }), + connectorType: text('connector_type').notNull(), + credentialId: text('credential_id').notNull(), + sourceConfig: json('source_config').notNull(), + syncMode: text('sync_mode').notNull().default('incremental'), + syncIntervalMinutes: integer('sync_interval_minutes').notNull().default(1440), + status: text('status').notNull().default('active'), + lastSyncAt: timestamp('last_sync_at'), + lastSyncError: text('last_sync_error'), + lastSyncDocCount: integer('last_sync_doc_count'), + nextSyncAt: timestamp('next_sync_at'), + consecutiveFailures: integer('consecutive_failures').notNull().default(0), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + deletedAt: timestamp('deleted_at'), + }, + (table) => ({ + knowledgeBaseIdIdx: index('kc_knowledge_base_id_idx').on(table.knowledgeBaseId), + // Cron scheduler: WHERE status='active' AND nextSyncAt <= now AND deletedAt IS NULL + statusNextSyncIdx: index('kc_status_next_sync_idx').on(table.status, table.nextSyncAt), + }) +) + +/** + * Knowledge Connector Sync Log - audit trail for connector sync operations. + */ +export const knowledgeConnectorSyncLog = pgTable( + 'knowledge_connector_sync_log', + { + id: text('id').primaryKey(), + connectorId: text('connector_id') + .notNull() + .references(() => knowledgeConnector.id, { onDelete: 'cascade' }), + status: text('status').notNull(), + startedAt: timestamp('started_at').notNull().defaultNow(), + completedAt: timestamp('completed_at'), + docsAdded: integer('docs_added').notNull().default(0), + docsUpdated: integer('docs_updated').notNull().default(0), + docsDeleted: integer('docs_deleted').notNull().default(0), + docsUnchanged: integer('docs_unchanged').notNull().default(0), + errorMessage: text('error_message'), + }, + (table) => ({ + connectorIdIdx: index('kcsl_connector_id_idx').on(table.connectorId), + }) +)