diff --git a/services/platform/convex/_generated/api.d.ts b/services/platform/convex/_generated/api.d.ts index 57084e314..1987ba122 100644 --- a/services/platform/convex/_generated/api.d.ts +++ b/services/platform/convex/_generated/api.d.ts @@ -287,6 +287,8 @@ import type * as model_integrations_update_integration_internal from "../model/i import type * as model_integrations_update_integration_logic from "../model/integrations/update_integration_logic.js"; import type * as model_integrations_update_sync_stats from "../model/integrations/update_sync_stats.js"; import type * as model_integrations_validators from "../model/integrations/validators.js"; +import type * as model_members_index from "../model/members/index.js"; +import type * as model_members_validators from "../model/members/validators.js"; import type * as model_onedrive_create_sync_configs_logic from "../model/onedrive/create_sync_configs_logic.js"; import type * as model_onedrive_get_user_token_logic from "../model/onedrive/get_user_token_logic.js"; import type * as model_onedrive_index from "../model/onedrive/index.js"; @@ -296,6 +298,7 @@ import type * as model_onedrive_refresh_token_logic from "../model/onedrive/refr import type * as model_onedrive_update_sync_config_logic from "../model/onedrive/update_sync_config_logic.js"; import type * as model_onedrive_upload_and_create_document_logic from "../model/onedrive/upload_and_create_document_logic.js"; import type * as model_onedrive_upload_to_storage_logic from "../model/onedrive/upload_to_storage_logic.js"; +import type * as model_onedrive_validators from "../model/onedrive/validators.js"; import type * as model_organizations_create_organization from "../model/organizations/create_organization.js"; import type * as model_organizations_delete_organization from "../model/organizations/delete_organization.js"; import type * as model_organizations_delete_organization_logo from "../model/organizations/delete_organization_logo.js"; @@ -331,6 +334,7 @@ import type * as model_threads_get_thread_messages_streaming from "../model/thre import type * as model_threads_index from "../model/threads/index.js"; import type * as model_threads_list_threads from "../model/threads/list_threads.js"; import type * as model_threads_update_chat_thread from "../model/threads/update_chat_thread.js"; +import type * as model_threads_validators from "../model/threads/validators.js"; import type * as model_tone_of_voice_add_example_message from "../model/tone_of_voice/add_example_message.js"; import type * as model_tone_of_voice_delete_example_message from "../model/tone_of_voice/delete_example_message.js"; import type * as model_tone_of_voice_generate_tone_of_voice from "../model/tone_of_voice/generate_tone_of_voice.js"; @@ -357,6 +361,8 @@ import type * as model_users_get_user_by_email from "../model/users/get_user_by_ import type * as model_users_has_any_users from "../model/users/has_any_users.js"; import type * as model_users_index from "../model/users/index.js"; import type * as model_users_update_user_password from "../model/users/update_user_password.js"; +import type * as model_vendors_index from "../model/vendors/index.js"; +import type * as model_vendors_validators from "../model/vendors/validators.js"; import type * as model_websites_bulk_create_websites from "../model/websites/bulk_create_websites.js"; import type * as model_websites_bulk_upsert_pages from "../model/websites/bulk_upsert_pages.js"; import type * as model_websites_create_website from "../model/websites/create_website.js"; @@ -977,6 +983,8 @@ declare const fullApi: ApiFromModules<{ "model/integrations/update_integration_logic": typeof model_integrations_update_integration_logic; "model/integrations/update_sync_stats": typeof model_integrations_update_sync_stats; "model/integrations/validators": typeof model_integrations_validators; + "model/members/index": typeof model_members_index; + "model/members/validators": typeof model_members_validators; "model/onedrive/create_sync_configs_logic": typeof model_onedrive_create_sync_configs_logic; "model/onedrive/get_user_token_logic": typeof model_onedrive_get_user_token_logic; "model/onedrive/index": typeof model_onedrive_index; @@ -986,6 +994,7 @@ declare const fullApi: ApiFromModules<{ "model/onedrive/update_sync_config_logic": typeof model_onedrive_update_sync_config_logic; "model/onedrive/upload_and_create_document_logic": typeof model_onedrive_upload_and_create_document_logic; "model/onedrive/upload_to_storage_logic": typeof model_onedrive_upload_to_storage_logic; + "model/onedrive/validators": typeof model_onedrive_validators; "model/organizations/create_organization": typeof model_organizations_create_organization; "model/organizations/delete_organization": typeof model_organizations_delete_organization; "model/organizations/delete_organization_logo": typeof model_organizations_delete_organization_logo; @@ -1021,6 +1030,7 @@ declare const fullApi: ApiFromModules<{ "model/threads/index": typeof model_threads_index; "model/threads/list_threads": typeof model_threads_list_threads; "model/threads/update_chat_thread": typeof model_threads_update_chat_thread; + "model/threads/validators": typeof model_threads_validators; "model/tone_of_voice/add_example_message": typeof model_tone_of_voice_add_example_message; "model/tone_of_voice/delete_example_message": typeof model_tone_of_voice_delete_example_message; "model/tone_of_voice/generate_tone_of_voice": typeof model_tone_of_voice_generate_tone_of_voice; @@ -1047,6 +1057,8 @@ declare const fullApi: ApiFromModules<{ "model/users/has_any_users": typeof model_users_has_any_users; "model/users/index": typeof model_users_index; "model/users/update_user_password": typeof model_users_update_user_password; + "model/vendors/index": typeof model_vendors_index; + "model/vendors/validators": typeof model_vendors_validators; "model/websites/bulk_create_websites": typeof model_websites_bulk_create_websites; "model/websites/bulk_upsert_pages": typeof model_websites_bulk_upsert_pages; "model/websites/create_website": typeof model_websites_create_website; diff --git a/services/platform/convex/documents.ts b/services/platform/convex/documents.ts index 9fedb1f27..51ae923aa 100644 --- a/services/platform/convex/documents.ts +++ b/services/platform/convex/documents.ts @@ -26,6 +26,30 @@ import { documentItemValidator as DocumentItem, documentListResponseValidator as DocumentListResponse, documentRecordValidator as DocumentRecord, + sourceProviderValidator, + excelSheetValidator, + uploadBase64ResponseValidator, + readFileBase64ResponseValidator, + sourceTypeValidator, + outputFormatValidator, + pdfOptionsValidator, + imageOptionsValidator, + urlOptionsValidator, + generateDocumentResponseValidator, + membershipValidator, + signedUrlResponseValidator, + documentByExtensionItemValidator, + deleteFromRagResponseValidator, + uploadFileResponseValidator, + createOneDriveSyncConfigResponseValidator, + analyzePptxResponseValidator, + slideContentDataValidator, + pptxBrandingDataValidator, + generatePptxResponseValidator, + docxContentValidator, + generateDocxResponseValidator, + retryRagIndexingResponseValidator, + sortOrderValidator, } from './model/documents/validators'; import { ragAction } from './workflow/actions/rag/rag_action'; @@ -46,9 +70,7 @@ export const createDocument = internalMutation({ mimeType: v.optional(v.string()), extension: v.optional(v.string()), metadata: v.optional(v.any()), - sourceProvider: v.optional( - v.union(v.literal('onedrive'), v.literal('upload')), - ), + sourceProvider: v.optional(sourceProviderValidator), externalItemId: v.optional(v.string()), }, returns: v.object({ @@ -73,14 +95,7 @@ export const uploadBase64Internal = internalAction({ contentType: v.string(), dataBase64: v.string(), }, - returns: v.object({ - success: v.boolean(), - fileId: v.id('_storage'), - url: v.string(), - fileName: v.string(), - size: v.number(), - contentType: v.string(), - }), + returns: uploadBase64ResponseValidator, handler: async (ctx, args) => { return await DocumentsModel.uploadBase64ToStorage(ctx, args); }, @@ -97,15 +112,7 @@ export const uploadBase64Internal = internalAction({ export const generateExcelInternal = internalAction({ args: { fileName: v.string(), - sheets: v.array( - v.object({ - name: v.string(), - headers: v.array(v.string()), - rows: v.array( - v.array(v.union(v.string(), v.number(), v.boolean(), v.null())), - ), - }), - ), + sheets: v.array(excelSheetValidator), }, returns: v.object({ success: v.boolean(), @@ -173,13 +180,7 @@ export const readFileBase64Internal = internalAction({ args: { fileId: v.id('_storage'), }, - returns: v.object({ - success: v.boolean(), - fileId: v.id('_storage'), - dataBase64: v.string(), - contentType: v.string(), - size: v.number(), - }), + returns: readFileBase64ResponseValidator, handler: async (ctx, args) => { return await DocumentsModel.readFileBase64FromStorage(ctx, args); }, @@ -193,59 +194,16 @@ export const readFileBase64Internal = internalAction({ export const generateDocumentInternal = internalAction({ args: { fileName: v.string(), - sourceType: v.union( - v.literal('markdown'), - v.literal('html'), - v.literal('url'), - ), - outputFormat: v.union(v.literal('pdf'), v.literal('image')), + sourceType: sourceTypeValidator, + outputFormat: outputFormatValidator, content: v.string(), - pdfOptions: v.optional( - v.object({ - format: v.optional(v.string()), - landscape: v.optional(v.boolean()), - marginTop: v.optional(v.string()), - marginBottom: v.optional(v.string()), - marginLeft: v.optional(v.string()), - marginRight: v.optional(v.string()), - printBackground: v.optional(v.boolean()), - }), - ), - imageOptions: v.optional( - v.object({ - imageType: v.optional(v.string()), - quality: v.optional(v.number()), - fullPage: v.optional(v.boolean()), - width: v.optional(v.number()), - height: v.optional(v.number()), - scale: v.optional(v.number()), - }), - ), - urlOptions: v.optional( - v.object({ - waitUntil: v.optional( - v.union( - v.literal('load'), - v.literal('domcontentloaded'), - v.literal('networkidle'), - v.literal('commit'), - ), - ), - timeout: v.optional(v.number()), - }), - ), + pdfOptions: v.optional(pdfOptionsValidator), + imageOptions: v.optional(imageOptionsValidator), + urlOptions: v.optional(urlOptionsValidator), extraCss: v.optional(v.string()), wrapInTemplate: v.optional(v.boolean()), }, - returns: v.object({ - success: v.boolean(), - fileId: v.id('_storage'), - url: v.string(), - fileName: v.string(), - contentType: v.string(), - extension: v.string(), - size: v.number(), - }), + returns: generateDocumentResponseValidator, handler: async (ctx, args) => { return await DocumentsModel.generateDocument(ctx, args); }, @@ -270,9 +228,7 @@ export const getDocumentById = internalQuery({ export const queryDocuments = internalQuery({ args: { organizationId: v.string(), - sourceProvider: v.optional( - v.union(v.literal('onedrive'), v.literal('upload')), - ), + sourceProvider: v.optional(sourceProviderValidator), paginationOpts: cursorPaginationOptsValidator, }, returns: v.object({ @@ -314,9 +270,7 @@ export const updateDocumentInternal = internalMutation({ fileId: v.optional(v.id('_storage')), mimeType: v.optional(v.string()), extension: v.optional(v.string()), - sourceProvider: v.optional( - v.union(v.literal('onedrive'), v.literal('upload')), - ), + sourceProvider: v.optional(sourceProviderValidator), externalItemId: v.optional(v.string()), }, returns: v.null(), @@ -334,15 +288,7 @@ export const checkMembership = internalQuery({ organizationId: v.string(), userId: v.string(), }, - returns: v.union( - v.null(), - v.object({ - _id: v.string(), - organizationId: v.string(), - identityId: v.optional(v.string()), - role: v.optional(v.string()), - }), - ), + returns: v.union(v.null(), membershipValidator), handler: async (ctx, args) => { return await DocumentsModel.checkMembership(ctx, args); }, @@ -355,16 +301,7 @@ export const generateSignedUrl = internalQuery({ args: { documentId: v.id('documents'), }, - returns: v.union( - v.object({ - success: v.boolean(), - url: v.string(), - }), - v.object({ - success: v.boolean(), - error: v.string(), - }), - ), + returns: signedUrlResponseValidator, handler: async (ctx, args) => { return await DocumentsModel.generateSignedUrl(ctx, args.documentId); }, @@ -382,17 +319,7 @@ export const listDocumentsByExtension = internalQuery({ extension: v.string(), limit: v.optional(v.number()), }, - returns: v.array( - v.object({ - _id: v.id('documents'), - _creationTime: v.number(), - title: v.optional(v.string()), - fileId: v.optional(v.id('_storage')), - mimeType: v.optional(v.string()), - extension: v.optional(v.string()), - metadata: v.optional(v.any()), - }), - ), + returns: v.array(documentByExtensionItemValidator), handler: async (ctx, args) => { return await DocumentsModel.listDocumentsByExtension(ctx, args); }, @@ -410,13 +337,7 @@ export const deleteDocumentFromRagInternal = internalAction({ documentId: v.string(), mode: v.optional(v.union(v.literal('soft'), v.literal('hard'))), }, - returns: v.object({ - success: v.boolean(), - deletedCount: v.number(), - deletedDataIds: v.array(v.string()), - message: v.string(), - error: v.optional(v.string()), - }), + returns: deleteFromRagResponseValidator, handler: async (_ctx, args) => { const { getRagConfig } = await import( './workflow/actions/rag/helpers/get_rag_config' @@ -484,7 +405,7 @@ export const getDocuments = queryWithRLS({ query: v.optional(v.string()), folderPath: v.optional(v.string()), sortField: v.optional(v.string()), - sortOrder: v.optional(v.union(v.literal('asc'), v.literal('desc'))), + sortOrder: v.optional(sortOrderValidator), }, returns: DocumentListResponse, handler: async (ctx, args) => { @@ -612,12 +533,7 @@ export const uploadFile = action({ contentType: v.string(), metadata: v.optional(v.any()), }, - returns: v.object({ - success: v.boolean(), - fileId: v.optional(v.string()), - documentId: v.optional(v.string()), - error: v.optional(v.string()), - }), + returns: uploadFileResponseValidator, handler: async ( ctx, args, @@ -752,11 +668,7 @@ export const createOneDriveSyncConfig = mutationWithRLS({ targetBucket: v.string(), storagePrefix: v.optional(v.string()), }, - returns: v.object({ - success: v.boolean(), - configId: v.optional(v.string()), - error: v.optional(v.string()), - }), + returns: createOneDriveSyncConfigResponseValidator, handler: async (ctx, args) => { return await DocumentsModel.createOneDriveSyncConfig(ctx, args); }, @@ -773,57 +685,7 @@ export const analyzePptxInternal = internalAction({ args: { templateStorageId: v.id('_storage'), }, - returns: v.object({ - success: v.boolean(), - slideCount: v.number(), - slides: v.array( - v.object({ - slideNumber: v.number(), - layoutName: v.string(), - title: v.union(v.string(), v.null()), - subtitle: v.union(v.string(), v.null()), - textContent: v.array( - v.object({ - text: v.string(), - isPlaceholder: v.boolean(), - }), - ), - tables: v.array( - v.object({ - rowCount: v.number(), - columnCount: v.number(), - headers: v.array(v.string()), - rows: v.array(v.array(v.string())), - }), - ), - charts: v.array( - v.object({ - chartType: v.string(), - hasLegend: v.optional(v.boolean()), - seriesCount: v.optional(v.number()), - }), - ), - images: v.array( - v.object({ - width: v.optional(v.number()), - height: v.optional(v.number()), - }), - ), - }), - ), - availableLayouts: v.array(v.string()), - branding: v.object({ - slideWidth: v.optional(v.number()), - slideHeight: v.optional(v.number()), - titleFontName: v.optional(v.union(v.string(), v.null())), - bodyFontName: v.optional(v.union(v.string(), v.null())), - titleFontSize: v.optional(v.number()), - bodyFontSize: v.optional(v.number()), - primaryColor: v.optional(v.union(v.string(), v.null())), - secondaryColor: v.optional(v.union(v.string(), v.null())), - accentColor: v.optional(v.union(v.string(), v.null())), - }), - }), + returns: analyzePptxResponseValidator, handler: async (ctx, args) => { return await DocumentsModel.analyzePptx(ctx, { templateStorageId: args.templateStorageId, @@ -840,45 +702,11 @@ export const analyzePptxInternal = internalAction({ export const generatePptxInternal = internalAction({ args: { fileName: v.string(), - slidesContent: v.array( - v.object({ - title: v.optional(v.string()), - subtitle: v.optional(v.string()), - textContent: v.optional(v.array(v.string())), - bulletPoints: v.optional(v.array(v.string())), - tables: v.optional( - v.array( - v.object({ - headers: v.array(v.string()), - rows: v.array(v.array(v.string())), - }), - ), - ), - }), - ), - branding: v.optional( - v.object({ - slideWidth: v.optional(v.number()), - slideHeight: v.optional(v.number()), - titleFontName: v.optional(v.string()), - bodyFontName: v.optional(v.string()), - titleFontSize: v.optional(v.number()), - bodyFontSize: v.optional(v.number()), - primaryColor: v.optional(v.string()), - secondaryColor: v.optional(v.string()), - accentColor: v.optional(v.string()), - }), - ), + slidesContent: v.array(slideContentDataValidator), + branding: v.optional(pptxBrandingDataValidator), templateStorageId: v.id('_storage'), }, - returns: v.object({ - success: v.boolean(), - fileId: v.id('_storage'), - url: v.string(), - fileName: v.string(), - contentType: v.string(), - size: v.number(), - }), + returns: generatePptxResponseValidator, handler: async (ctx, args) => { return await DocumentsModel.generatePptx(ctx, { fileName: args.fileName, @@ -895,37 +723,9 @@ export const generatePptxInternal = internalAction({ export const generateDocxInternal = internalAction({ args: { fileName: v.string(), - content: v.object({ - title: v.optional(v.string()), - subtitle: v.optional(v.string()), - sections: v.array( - v.object({ - type: v.union( - v.literal('heading'), - v.literal('paragraph'), - v.literal('bullets'), - v.literal('numbered'), - v.literal('table'), - v.literal('quote'), - v.literal('code'), - ), - text: v.optional(v.string()), - level: v.optional(v.number()), - items: v.optional(v.array(v.string())), - headers: v.optional(v.array(v.string())), - rows: v.optional(v.array(v.array(v.string()))), - }), - ), - }), + content: docxContentValidator, }, - returns: v.object({ - success: v.boolean(), - fileId: v.id('_storage'), - url: v.string(), - fileName: v.string(), - contentType: v.string(), - size: v.number(), - }), + returns: generateDocxResponseValidator, handler: async (ctx, args) => { return await DocumentsModel.generateDocx(ctx, { fileName: args.fileName, @@ -945,38 +745,10 @@ export const generateDocxInternal = internalAction({ export const generateDocxFromTemplateInternal = internalAction({ args: { fileName: v.string(), - content: v.object({ - title: v.optional(v.string()), - subtitle: v.optional(v.string()), - sections: v.array( - v.object({ - type: v.union( - v.literal('heading'), - v.literal('paragraph'), - v.literal('bullets'), - v.literal('numbered'), - v.literal('table'), - v.literal('quote'), - v.literal('code'), - ), - text: v.optional(v.string()), - level: v.optional(v.number()), - items: v.optional(v.array(v.string())), - headers: v.optional(v.array(v.string())), - rows: v.optional(v.array(v.array(v.string()))), - }), - ), - }), + content: docxContentValidator, templateStorageId: v.id('_storage'), }, - returns: v.object({ - success: v.boolean(), - fileId: v.id('_storage'), - url: v.string(), - fileName: v.string(), - contentType: v.string(), - size: v.number(), - }), + returns: generateDocxResponseValidator, handler: async (ctx, args) => { return await DocumentsModel.generateDocxFromTemplate(ctx, { fileName: args.fileName, @@ -996,11 +768,7 @@ export const generateDocxFromTemplateInternal = internalAction({ */ export const retryRagIndexing = action({ args: { documentId: v.id('documents') }, - returns: v.object({ - success: v.boolean(), - jobId: v.optional(v.string()), - error: v.optional(v.string()), - }), + returns: retryRagIndexingResponseValidator, handler: async (ctx, args) => { try { const identity = await ctx.auth.getUserIdentity(); diff --git a/services/platform/convex/member.ts b/services/platform/convex/member.ts index fc8b5c713..35b096ce8 100644 --- a/services/platform/convex/member.ts +++ b/services/platform/convex/member.ts @@ -3,7 +3,13 @@ import { queryWithRLS, mutationWithRLS, getAuthenticatedUser } from './lib/rls'; import { query } from './_generated/server'; import { components } from './_generated/api'; -const _role = v.string(); +// Import validators from model +import { + sortOrderValidator, + memberListItemValidator, + memberContextValidator, + addMemberResponseValidator, +} from './model/members/validators'; /** * Public query to get user ID by email (for adding existing users to organizations). @@ -47,21 +53,10 @@ export const getUserIdByEmail = query({ export const listByOrganization = queryWithRLS({ args: { organizationId: v.string(), - sortOrder: v.optional(v.union(v.literal('asc'), v.literal('desc'))), + sortOrder: v.optional(sortOrderValidator), search: v.optional(v.string()), }, - returns: v.array( - v.object({ - _id: v.string(), - _creationTime: v.number(), - organizationId: v.string(), - identityId: v.optional(v.string()), - email: v.optional(v.string()), - role: v.optional(v.string()), - displayName: v.optional(v.string()), - metadata: v.optional(v.any()), - }), - ), + returns: v.array(memberListItemValidator), handler: async (ctx, args) => { // RLS handles authorization automatically const result = await ctx.runQuery(components.betterAuth.adapter.findMany, { @@ -127,24 +122,7 @@ export const getCurrentMemberContext = queryWithRLS({ args: { organizationId: v.string(), }, - returns: v.object({ - member: v.union( - v.object({ - _id: v.string(), - _creationTime: v.number(), - organizationId: v.string(), - identityId: v.optional(v.string()), - email: v.optional(v.string()), - role: v.optional(v.string()), - displayName: v.optional(v.string()), - }), - v.null(), - ), - role: v.union(v.string(), v.null()), - isAdmin: v.boolean(), - canManageMembers: v.boolean(), - canChangePassword: v.boolean(), - }), + returns: memberContextValidator, handler: async (ctx, args) => { // Get current authenticated user through RLS context const authUser = await getAuthenticatedUser(ctx); @@ -257,9 +235,7 @@ export const addMember = mutationWithRLS({ role: v.optional(v.string()), displayName: v.optional(v.string()), }, - returns: v.object({ - memberId: v.string(), - }), + returns: addMemberResponseValidator, handler: async (ctx, args) => { const trustedHeadersEnabled = process.env.TRUSTED_HEADERS_ENABLED === 'true'; diff --git a/services/platform/convex/model/documents/validators.ts b/services/platform/convex/model/documents/validators.ts index 516f2a24f..90c5c0f08 100644 --- a/services/platform/convex/model/documents/validators.ts +++ b/services/platform/convex/model/documents/validators.ts @@ -101,3 +101,370 @@ export const documentRecordValidator = v.object({ externalItemId: v.optional(v.string()), }); +/** + * Sort order validator + */ +export const sortOrderValidator = v.union(v.literal('asc'), v.literal('desc')); + +/** + * Excel sheet validator (for generateExcelInternal) + */ +export const excelSheetValidator = v.object({ + name: v.string(), + headers: v.array(v.string()), + rows: v.array( + v.array(v.union(v.string(), v.number(), v.boolean(), v.null())), + ), +}); + +/** + * Generate Excel response validator + */ +export const generateExcelResponseValidator = v.object({ + success: v.boolean(), + fileId: v.id('_storage'), + url: v.string(), + fileName: v.string(), + rowCount: v.number(), + sheetCount: v.number(), +}); + +/** + * Upload base64 response validator + */ +export const uploadBase64ResponseValidator = v.object({ + success: v.boolean(), + fileId: v.id('_storage'), + url: v.string(), + fileName: v.string(), + size: v.number(), + contentType: v.string(), +}); + +/** + * Read file base64 response validator + */ +export const readFileBase64ResponseValidator = v.object({ + success: v.boolean(), + fileId: v.id('_storage'), + dataBase64: v.string(), + contentType: v.string(), + size: v.number(), +}); + +/** + * Source type validator (for document generation) + */ +export const sourceTypeValidator = v.union( + v.literal('markdown'), + v.literal('html'), + v.literal('url'), +); + +/** + * Output format validator (for document generation) + */ +export const outputFormatValidator = v.union( + v.literal('pdf'), + v.literal('image'), +); + +/** + * PDF options validator + */ +export const pdfOptionsValidator = v.object({ + format: v.optional(v.string()), + landscape: v.optional(v.boolean()), + marginTop: v.optional(v.string()), + marginBottom: v.optional(v.string()), + marginLeft: v.optional(v.string()), + marginRight: v.optional(v.string()), + printBackground: v.optional(v.boolean()), +}); + +/** + * Image options validator + */ +export const imageOptionsValidator = v.object({ + imageType: v.optional(v.string()), + quality: v.optional(v.number()), + fullPage: v.optional(v.boolean()), + width: v.optional(v.number()), + height: v.optional(v.number()), + scale: v.optional(v.number()), +}); + +/** + * Wait until validator for URL options + */ +export const waitUntilValidator = v.union( + v.literal('load'), + v.literal('domcontentloaded'), + v.literal('networkidle'), + v.literal('commit'), +); + +/** + * URL options validator + */ +export const urlOptionsValidator = v.object({ + waitUntil: v.optional(waitUntilValidator), + timeout: v.optional(v.number()), +}); + +/** + * Generate document response validator + */ +export const generateDocumentResponseValidator = v.object({ + success: v.boolean(), + fileId: v.id('_storage'), + url: v.string(), + fileName: v.string(), + contentType: v.string(), + extension: v.string(), + size: v.number(), +}); + +/** + * Membership validator (for checkMembership) + */ +export const membershipValidator = v.object({ + _id: v.string(), + organizationId: v.string(), + identityId: v.optional(v.string()), + role: v.optional(v.string()), +}); + +/** + * Signed URL response validator + */ +export const signedUrlResponseValidator = v.union( + v.object({ + success: v.boolean(), + url: v.string(), + }), + v.object({ + success: v.boolean(), + error: v.string(), + }), +); + +/** + * Document by extension item validator + */ +export const documentByExtensionItemValidator = v.object({ + _id: v.id('documents'), + _creationTime: v.number(), + title: v.optional(v.string()), + fileId: v.optional(v.id('_storage')), + mimeType: v.optional(v.string()), + extension: v.optional(v.string()), + metadata: v.optional(v.any()), +}); + +/** + * Delete from RAG response validator + */ +export const deleteFromRagResponseValidator = v.object({ + success: v.boolean(), + deletedCount: v.number(), + deletedDataIds: v.array(v.string()), + message: v.string(), + error: v.optional(v.string()), +}); + +/** + * Upload file response validator + */ +export const uploadFileResponseValidator = v.object({ + success: v.boolean(), + fileId: v.optional(v.string()), + documentId: v.optional(v.string()), + error: v.optional(v.string()), +}); + +/** + * Create OneDrive sync config response validator + */ +export const createOneDriveSyncConfigResponseValidator = v.object({ + success: v.boolean(), + configId: v.optional(v.string()), + error: v.optional(v.string()), +}); + +/** + * Text content info validator (for PPTX analysis) + */ +export const textContentInfoValidator = v.object({ + text: v.string(), + isPlaceholder: v.boolean(), +}); + +/** + * Table info validator (for PPTX analysis) + */ +export const tableInfoValidator = v.object({ + rowCount: v.number(), + columnCount: v.number(), + headers: v.array(v.string()), + rows: v.array(v.array(v.string())), +}); + +/** + * Chart info validator (for PPTX analysis) + */ +export const chartInfoValidator = v.object({ + chartType: v.string(), + hasLegend: v.optional(v.boolean()), + seriesCount: v.optional(v.number()), +}); + +/** + * Image info validator (for PPTX analysis) + */ +export const imageInfoValidator = v.object({ + width: v.optional(v.number()), + height: v.optional(v.number()), +}); + +/** + * Slide info validator (for PPTX analysis) + */ +export const slideInfoValidator = v.object({ + slideNumber: v.number(), + layoutName: v.string(), + title: v.union(v.string(), v.null()), + subtitle: v.union(v.string(), v.null()), + textContent: v.array(textContentInfoValidator), + tables: v.array(tableInfoValidator), + charts: v.array(chartInfoValidator), + images: v.array(imageInfoValidator), +}); + +/** + * Branding info validator (for PPTX analysis) + */ +export const brandingInfoValidator = v.object({ + slideWidth: v.optional(v.number()), + slideHeight: v.optional(v.number()), + titleFontName: v.optional(v.union(v.string(), v.null())), + bodyFontName: v.optional(v.union(v.string(), v.null())), + titleFontSize: v.optional(v.number()), + bodyFontSize: v.optional(v.number()), + primaryColor: v.optional(v.union(v.string(), v.null())), + secondaryColor: v.optional(v.union(v.string(), v.null())), + accentColor: v.optional(v.union(v.string(), v.null())), +}); + +/** + * Analyze PPTX response validator + */ +export const analyzePptxResponseValidator = v.object({ + success: v.boolean(), + slideCount: v.number(), + slides: v.array(slideInfoValidator), + availableLayouts: v.array(v.string()), + branding: brandingInfoValidator, +}); + +/** + * Table data validator (for PPTX generation) + */ +export const tableDataValidator = v.object({ + headers: v.array(v.string()), + rows: v.array(v.array(v.string())), +}); + +/** + * Slide content data validator (for PPTX generation) + */ +export const slideContentDataValidator = v.object({ + title: v.optional(v.string()), + subtitle: v.optional(v.string()), + textContent: v.optional(v.array(v.string())), + bulletPoints: v.optional(v.array(v.string())), + tables: v.optional(v.array(tableDataValidator)), +}); + +/** + * PPTX branding data validator (for PPTX generation) + */ +export const pptxBrandingDataValidator = v.object({ + slideWidth: v.optional(v.number()), + slideHeight: v.optional(v.number()), + titleFontName: v.optional(v.string()), + bodyFontName: v.optional(v.string()), + titleFontSize: v.optional(v.number()), + bodyFontSize: v.optional(v.number()), + primaryColor: v.optional(v.string()), + secondaryColor: v.optional(v.string()), + accentColor: v.optional(v.string()), +}); + +/** + * Generate PPTX response validator + */ +export const generatePptxResponseValidator = v.object({ + success: v.boolean(), + fileId: v.id('_storage'), + url: v.string(), + fileName: v.string(), + contentType: v.string(), + size: v.number(), +}); + +/** + * DOCX section type validator + */ +export const docxSectionTypeValidator = v.union( + v.literal('heading'), + v.literal('paragraph'), + v.literal('bullets'), + v.literal('numbered'), + v.literal('table'), + v.literal('quote'), + v.literal('code'), +); + +/** + * DOCX section validator + */ +export const docxSectionValidator = v.object({ + type: docxSectionTypeValidator, + text: v.optional(v.string()), + level: v.optional(v.number()), + items: v.optional(v.array(v.string())), + headers: v.optional(v.array(v.string())), + rows: v.optional(v.array(v.array(v.string()))), +}); + +/** + * DOCX content validator (for DOCX generation) + */ +export const docxContentValidator = v.object({ + title: v.optional(v.string()), + subtitle: v.optional(v.string()), + sections: v.array(docxSectionValidator), +}); + +/** + * Generate DOCX response validator + */ +export const generateDocxResponseValidator = v.object({ + success: v.boolean(), + fileId: v.id('_storage'), + url: v.string(), + fileName: v.string(), + contentType: v.string(), + size: v.number(), +}); + +/** + * Retry RAG indexing response validator + */ +export const retryRagIndexingResponseValidator = v.object({ + success: v.boolean(), + jobId: v.optional(v.string()), + error: v.optional(v.string()), +}); + diff --git a/services/platform/convex/model/email_providers/validators.ts b/services/platform/convex/model/email_providers/validators.ts index 81c40ff89..e9890a497 100644 --- a/services/platform/convex/model/email_providers/validators.ts +++ b/services/platform/convex/model/email_providers/validators.ts @@ -35,6 +35,106 @@ export const imapConfigValidator = v.object({ secure: v.boolean(), }); +/** + * Send method validator (SMTP or API) + */ +export const sendMethodValidator = v.union( + v.literal('smtp'), + v.literal('api'), +); + +/** + * Account type validator (for OAuth2) + */ +export const accountTypeValidator = v.union( + v.literal('personal'), + v.literal('organizational'), + v.literal('both'), +); + +/** + * Password auth validator + */ +export const passwordAuthValidator = v.object({ + user: v.string(), + pass: v.string(), +}); + +/** + * Password auth encrypted validator (for internal storage) + */ +export const passwordAuthEncryptedValidator = v.object({ + user: v.string(), + passEncrypted: v.string(), +}); + +/** + * OAuth2 auth validator (for input) + */ +export const oauth2AuthInputValidator = v.object({ + provider: v.string(), + clientId: v.string(), + clientSecret: v.string(), + tokenUrl: v.optional(v.string()), +}); + +/** + * OAuth2 auth validator (for internal storage with encrypted fields) + */ +export const oauth2AuthStoredValidator = v.object({ + provider: v.string(), + clientId: v.string(), + clientSecretEncrypted: v.string(), + accessTokenEncrypted: v.optional(v.string()), + refreshTokenEncrypted: v.optional(v.string()), + tokenExpiry: v.optional(v.number()), + tokenUrl: v.optional(v.string()), +}); + +/** + * OAuth2 auth simple validator (for testing connection) + */ +export const oauth2AuthSimpleValidator = v.object({ + user: v.string(), + accessToken: v.string(), +}); + +/** + * Connection test result item validator + */ +export const connectionTestItemValidator = v.object({ + success: v.boolean(), + latencyMs: v.number(), + error: v.optional(v.string()), +}); + +/** + * Connection test result validator + */ +export const connectionTestResultValidator = v.object({ + success: v.boolean(), + smtp: connectionTestItemValidator, + imap: connectionTestItemValidator, +}); + +/** + * OAuth2 callback response validator + */ +export const oauth2CallbackResponseValidator = v.object({ + success: v.boolean(), + userEmail: v.optional(v.string()), + redirectUri: v.optional(v.string()), + redirectOrigin: v.optional(v.string()), +}); + +/** + * Send message response validator + */ +export const sendMessageResponseValidator = v.object({ + success: v.boolean(), + messageId: v.optional(v.string()), +}); + /** * Validator for email provider document (for query returns) */ @@ -45,24 +145,9 @@ export const emailProviderDocValidator = v.object({ name: v.string(), vendor: emailProviderVendorValidator, authMethod: emailProviderAuthMethodValidator, - sendMethod: v.optional(v.union(v.literal('smtp'), v.literal('api'))), - passwordAuth: v.optional( - v.object({ - user: v.string(), - passEncrypted: v.string(), - }), - ), - oauth2Auth: v.optional( - v.object({ - provider: v.string(), - clientId: v.string(), - clientSecretEncrypted: v.string(), - accessTokenEncrypted: v.optional(v.string()), - refreshTokenEncrypted: v.optional(v.string()), - tokenExpiry: v.optional(v.number()), - tokenUrl: v.optional(v.string()), - }), - ), + sendMethod: v.optional(sendMethodValidator), + passwordAuth: v.optional(passwordAuthEncryptedValidator), + oauth2Auth: v.optional(oauth2AuthStoredValidator), smtpConfig: v.optional(smtpConfigValidator), imapConfig: v.optional(imapConfigValidator), isActive: v.optional(v.boolean()), diff --git a/services/platform/convex/model/members/index.ts b/services/platform/convex/model/members/index.ts new file mode 100644 index 000000000..27d3e458f --- /dev/null +++ b/services/platform/convex/model/members/index.ts @@ -0,0 +1,6 @@ +/** + * Members Model - Validators for member operations + */ + +// Export validators +export * from './validators'; diff --git a/services/platform/convex/model/members/validators.ts b/services/platform/convex/model/members/validators.ts new file mode 100644 index 000000000..ae036587e --- /dev/null +++ b/services/platform/convex/model/members/validators.ts @@ -0,0 +1,57 @@ +/** + * Convex validators for members model + */ + +import { v } from 'convex/values'; + +/** + * Sort order validator + */ +export const sortOrderValidator = v.union(v.literal('asc'), v.literal('desc')); + +/** + * Member list item validator (for listing organization members) + * Note: _id is a string because members come from Better Auth adapter, not native Convex tables + */ +export const memberListItemValidator = v.object({ + _id: v.string(), + _creationTime: v.number(), + organizationId: v.string(), + identityId: v.optional(v.string()), + email: v.optional(v.string()), + role: v.optional(v.string()), + displayName: v.optional(v.string()), + metadata: v.optional(v.any()), +}); + +/** + * Member object validator (for member details) + * Note: _id is a string because members come from Better Auth adapter, not native Convex tables + */ +export const memberValidator = v.object({ + _id: v.string(), + _creationTime: v.number(), + organizationId: v.string(), + identityId: v.optional(v.string()), + email: v.optional(v.string()), + role: v.optional(v.string()), + displayName: v.optional(v.string()), +}); + +/** + * Member context response validator (for getCurrentMemberContext) + */ +export const memberContextValidator = v.object({ + member: v.union(memberValidator, v.null()), + role: v.union(v.string(), v.null()), + isAdmin: v.boolean(), + canManageMembers: v.boolean(), + canChangePassword: v.boolean(), +}); + +/** + * Add member response validator + */ +export const addMemberResponseValidator = v.object({ + memberId: v.string(), +}); diff --git a/services/platform/convex/model/onedrive/index.ts b/services/platform/convex/model/onedrive/index.ts index 4e6582ee4..21973d116 100644 --- a/services/platform/convex/model/onedrive/index.ts +++ b/services/platform/convex/model/onedrive/index.ts @@ -2,6 +2,9 @@ * OneDrive Model - Business logic for OneDrive operations */ +// Export validators +export * from './validators'; + export * from './get_user_token_logic'; export * from './refresh_token_logic'; export * from './read_file_logic'; diff --git a/services/platform/convex/model/onedrive/validators.ts b/services/platform/convex/model/onedrive/validators.ts new file mode 100644 index 000000000..a63bd6231 --- /dev/null +++ b/services/platform/convex/model/onedrive/validators.ts @@ -0,0 +1,164 @@ +/** + * Convex validators for OneDrive model + */ + +import { v } from 'convex/values'; + +/** + * OneDrive sync config status validator + */ +export const syncConfigStatusValidator = v.union( + v.literal('active'), + v.literal('inactive'), + v.literal('error'), +); + +/** + * OneDrive item type validator + */ +export const onedriveItemTypeValidator = v.union( + v.literal('file'), + v.literal('folder'), +); + +/** + * File hash validator for OneDrive files + */ +export const fileHashValidator = v.object({ + sha1Hash: v.optional(v.string()), + sha256Hash: v.optional(v.string()), +}); + +/** + * File metadata validator for OneDrive files + */ +export const onedriveFileMetadataValidator = v.object({ + mimeType: v.string(), + hashes: v.optional(fileHashValidator), +}); + +/** + * Folder metadata validator for OneDrive folders + */ +export const onedriveFolderMetadataValidator = v.object({ + childCount: v.number(), +}); + +/** + * Parent reference validator for OneDrive items + */ +export const parentReferenceValidator = v.object({ + driveId: v.string(), + driveType: v.string(), + id: v.string(), + path: v.string(), +}); + +/** + * Drive item validator (single OneDrive item) + */ +export const driveItemValidator = v.object({ + id: v.string(), + name: v.string(), + size: v.optional(v.number()), + createdDateTime: v.string(), + lastModifiedDateTime: v.string(), + webUrl: v.string(), + downloadUrl: v.optional(v.string()), + file: v.optional(onedriveFileMetadataValidator), + folder: v.optional(onedriveFolderMetadataValidator), + parentReference: v.optional(parentReferenceValidator), +}); + +/** + * Drive items response validator (paginated list of items) + */ +export const driveItemsResponseValidator = v.object({ + nextLink: v.optional(v.string()), + value: v.array(driveItemValidator), +}); + +/** + * File item validator (simplified for folder contents listing) + */ +export const fileItemValidator = v.object({ + id: v.string(), + name: v.string(), + size: v.number(), + mimeType: v.optional(v.string()), + lastModified: v.optional(v.number()), + isFolder: v.boolean(), +}); + +/** + * List files response validator + */ +export const listFilesResponseValidator = v.object({ + success: v.boolean(), + data: v.optional(driveItemsResponseValidator), + error: v.optional(v.string()), +}); + +/** + * Read file response validator + */ +export const readFileResponseValidator = v.object({ + success: v.boolean(), + data: v.optional( + v.object({ + content: v.bytes(), + mimeType: v.string(), + size: v.number(), + }), + ), + error: v.optional(v.string()), +}); + +/** + * Read file from OneDrive response validator (internal action) + */ +export const readFileFromOnedriveResponseValidator = v.object({ + success: v.boolean(), + content: v.optional(v.bytes()), + mimeType: v.optional(v.string()), + size: v.optional(v.number()), + error: v.optional(v.string()), +}); + +/** + * List folder contents response validator (internal action) + */ +export const listFolderContentsResponseValidator = v.object({ + success: v.boolean(), + files: v.optional(v.array(fileItemValidator)), + error: v.optional(v.string()), +}); + +/** + * Upload to storage response validator (internal action) + */ +export const uploadToStorageResponseValidator = v.object({ + success: v.boolean(), + fileId: v.optional(v.string()), + documentId: v.optional(v.string()), + error: v.optional(v.string()), +}); + +/** + * Refresh token response validator + */ +export const refreshTokenResponseValidator = v.object({ + success: v.boolean(), + accessToken: v.optional(v.string()), + error: v.optional(v.string()), +}); + +/** + * Get user token response validator (internal query) + */ +export const getUserTokenResponseValidator = v.object({ + token: v.union(v.string(), v.null()), + needsRefresh: v.boolean(), + accountId: v.union(v.string(), v.null()), + refreshToken: v.union(v.string(), v.null()), +}); diff --git a/services/platform/convex/model/threads/index.ts b/services/platform/convex/model/threads/index.ts index efd4593e1..bfef9eff1 100644 --- a/services/platform/convex/model/threads/index.ts +++ b/services/platform/convex/model/threads/index.ts @@ -2,6 +2,9 @@ * Threads model - Business logic for thread operations */ +// Export validators +export * from './validators'; + export { createChatThread } from './create_chat_thread'; export { deleteChatThread } from './delete_chat_thread'; export { getThreadMessages } from './get_thread_messages'; diff --git a/services/platform/convex/model/threads/validators.ts b/services/platform/convex/model/threads/validators.ts new file mode 100644 index 000000000..393a743ea --- /dev/null +++ b/services/platform/convex/model/threads/validators.ts @@ -0,0 +1,77 @@ +/** + * Convex validators for threads model + */ + +import { v } from 'convex/values'; + +/** + * Chat type validator for thread creation + */ +export const chatTypeValidator = v.union( + v.literal('general'), + v.literal('workflow_assistant'), +); + +/** + * Message role validator for thread messages + */ +export const messageRoleValidator = v.union( + v.literal('user'), + v.literal('assistant'), +); + +/** + * Thread status validator + */ +export const threadStatusValidator = v.union( + v.literal('active'), + v.literal('archived'), +); + +/** + * Tool execution status validator + */ +export const toolStatusValidator = v.union( + v.literal('calling'), + v.literal('completed'), + v.null(), +); + +/** + * Thread message validator (single message in a thread) + * Note: _id is a string because messages come from Agent Component, not native Convex tables + */ +export const threadMessageValidator = v.object({ + _id: v.string(), + _creationTime: v.number(), + role: messageRoleValidator, + content: v.string(), +}); + +/** + * Thread messages response validator + */ +export const threadMessagesResponseValidator = v.object({ + messages: v.array(threadMessageValidator), +}); + +/** + * Thread list item validator (for listing threads) + * Note: _id is a string because threads come from Agent Component, not native Convex tables + */ +export const threadListItemValidator = v.object({ + _id: v.string(), + _creationTime: v.number(), + title: v.optional(v.string()), + status: threadStatusValidator, + userId: v.optional(v.string()), +}); + +/** + * Latest tool message validator + */ +export const latestToolMessageValidator = v.object({ + toolNames: v.array(v.string()), + status: toolStatusValidator, + timestamp: v.union(v.number(), v.null()), +}); diff --git a/services/platform/convex/model/vendors/index.ts b/services/platform/convex/model/vendors/index.ts new file mode 100644 index 000000000..e711204bf --- /dev/null +++ b/services/platform/convex/model/vendors/index.ts @@ -0,0 +1,6 @@ +/** + * Vendors Model - Validators for vendor operations + */ + +// Export validators +export * from './validators'; diff --git a/services/platform/convex/model/vendors/validators.ts b/services/platform/convex/model/vendors/validators.ts new file mode 100644 index 000000000..e0758434c --- /dev/null +++ b/services/platform/convex/model/vendors/validators.ts @@ -0,0 +1,77 @@ +/** + * Convex validators for vendors model + */ + +import { v } from 'convex/values'; + +/** + * Sort order validator + */ +export const sortOrderValidator = v.union(v.literal('asc'), v.literal('desc')); + +/** + * Vendor source validator + */ +export const vendorSourceValidator = v.union( + v.literal('manual_import'), + v.literal('file_upload'), + v.literal('circuly'), +); + +/** + * Vendor address validator + */ +export const vendorAddressValidator = v.object({ + street: v.optional(v.string()), + city: v.optional(v.string()), + state: v.optional(v.string()), + country: v.optional(v.string()), + postalCode: v.optional(v.string()), +}); + +/** + * Vendor input validator (for creating/updating vendors) + */ +export const vendorInputValidator = v.object({ + name: v.optional(v.string()), + email: v.string(), + phone: v.optional(v.string()), + externalId: v.optional(v.string()), + source: vendorSourceValidator, + locale: v.optional(v.string()), + address: v.optional(vendorAddressValidator), + tags: v.optional(v.array(v.string())), + metadata: v.optional(v.any()), + notes: v.optional(v.string()), +}); + +/** + * Vendor list response validator (paginated list with metadata) + */ +export const vendorListResponseValidator = v.object({ + items: v.array(v.any()), + total: v.number(), + page: v.number(), + pageSize: v.number(), + totalPages: v.number(), + hasNextPage: v.boolean(), + hasPreviousPage: v.boolean(), +}); + +/** + * Bulk create error item validator + */ +export const bulkCreateErrorItemValidator = v.object({ + index: v.number(), + error: v.string(), + vendor: v.any(), +}); + +/** + * Bulk create vendors response validator + */ +export const bulkCreateVendorsResponseValidator = v.object({ + success: v.number(), + failed: v.number(), + errors: v.array(bulkCreateErrorItemValidator), +}); diff --git a/services/platform/convex/onedrive.ts b/services/platform/convex/onedrive.ts index a58b248ff..9f0259857 100644 --- a/services/platform/convex/onedrive.ts +++ b/services/platform/convex/onedrive.ts @@ -19,6 +19,20 @@ import type { ActionCtx, QueryCtx } from './_generated/server'; import type { Id } from './_generated/dataModel'; import { checkUserRateLimit } from './lib/rate_limiter/helpers'; +// Import validators from model +import { + driveItemsResponseValidator, + syncConfigStatusValidator, + fileItemValidator, + listFilesResponseValidator, + readFileResponseValidator, + readFileFromOnedriveResponseValidator, + listFolderContentsResponseValidator, + uploadToStorageResponseValidator, + refreshTokenResponseValidator, + getUserTokenResponseValidator, +} from './model/onedrive/validators'; + // ============================================================================= // TYPES // ============================================================================= @@ -65,49 +79,6 @@ interface MicrosoftAccount { refreshTokenExpiresAt: number | null; } -// ============================================================================= -// VALIDATORS -// ============================================================================= - -const driveItemValidator = v.object({ - id: v.string(), - name: v.string(), - size: v.optional(v.number()), - createdDateTime: v.string(), - lastModifiedDateTime: v.string(), - webUrl: v.string(), - downloadUrl: v.optional(v.string()), - file: v.optional( - v.object({ - mimeType: v.string(), - hashes: v.optional( - v.object({ - sha1Hash: v.optional(v.string()), - sha256Hash: v.optional(v.string()), - }), - ), - }), - ), - folder: v.optional( - v.object({ - childCount: v.number(), - }), - ), - parentReference: v.optional( - v.object({ - driveId: v.string(), - driveType: v.string(), - id: v.string(), - path: v.string(), - }), - ), -}); - -const driveItemsResponseValidator = v.object({ - nextLink: v.optional(v.string()), - value: v.array(driveItemValidator), -}); - // ============================================================================= // INTERNAL HELPERS // ============================================================================= @@ -942,9 +913,7 @@ export const uploadToStorage = internalAction({ export const updateSyncConfig = internalMutation({ args: { configId: v.id('onedriveSyncConfigs'), - status: v.optional( - v.union(v.literal('active'), v.literal('inactive'), v.literal('error')), - ), + status: v.optional(syncConfigStatusValidator), lastSyncAt: v.optional(v.number()), lastSyncStatus: v.optional(v.string()), errorMessage: v.optional(v.string()), diff --git a/services/platform/convex/threads.ts b/services/platform/convex/threads.ts index e5025ea21..cfa6a833c 100644 --- a/services/platform/convex/threads.ts +++ b/services/platform/convex/threads.ts @@ -12,6 +12,14 @@ import { vStreamArgs } from '@convex-dev/agent'; import { validateOrganizationAccess, getAuthenticatedUser } from './lib/rls'; import * as ThreadsModel from './model/threads'; +// Import validators from model +import { + chatTypeValidator, + threadMessagesResponseValidator, + threadListItemValidator, + latestToolMessageValidator, +} from './model/threads/validators'; + /** * Create a new chat thread using Convex Agent Component. * Simply wraps createThread and validates organization access. @@ -20,9 +28,7 @@ export const createChatThread = mutation({ args: { organizationId: v.string(), title: v.optional(v.string()), - chatType: v.optional( - v.union(v.literal('general'), v.literal('workflow_assistant')), - ), + chatType: v.optional(chatTypeValidator), }, returns: v.string(), handler: async (ctx, args) => { @@ -72,16 +78,7 @@ export const getThreadMessages = query({ args: { threadId: v.string(), }, - returns: v.object({ - messages: v.array( - v.object({ - _id: v.string(), - _creationTime: v.number(), - role: v.union(v.literal('user'), v.literal('assistant')), - content: v.string(), - }), - ), - }), + returns: threadMessagesResponseValidator, handler: async (ctx, args) => { return await ThreadsModel.getThreadMessages(ctx, args.threadId); }, @@ -95,15 +92,7 @@ export const listThreads = query({ args: { search: v.optional(v.string()), }, - returns: v.array( - v.object({ - _id: v.string(), - _creationTime: v.number(), - title: v.optional(v.string()), - status: v.union(v.literal('active'), v.literal('archived')), - userId: v.optional(v.string()), - }), - ), + returns: v.array(threadListItemValidator), handler: async (ctx, args) => { const user = await getAuthenticatedUser(ctx); if (!user) return []; @@ -145,11 +134,7 @@ export const getLatestToolMessage = query({ args: { threadId: v.string(), }, - returns: v.object({ - toolNames: v.array(v.string()), - status: v.union(v.literal('calling'), v.literal('completed'), v.null()), - timestamp: v.union(v.number(), v.null()), - }), + returns: latestToolMessageValidator, handler: async (ctx, args) => { return await ThreadsModel.getLatestToolMessage(ctx, args.threadId); }, diff --git a/services/platform/convex/vendors.ts b/services/platform/convex/vendors.ts index 22338faad..102a79cbd 100644 --- a/services/platform/convex/vendors.ts +++ b/services/platform/convex/vendors.ts @@ -8,6 +8,16 @@ import { } from './lib/pagination'; import type { Doc } from './_generated/dataModel'; +// Import validators from model +import { + sortOrderValidator, + vendorSourceValidator, + vendorAddressValidator, + vendorInputValidator, + vendorListResponseValidator, + bulkCreateVendorsResponseValidator, +} from './model/vendors/validators'; + /** * Check if organization has any vendors (fast count query for empty state detection) */ @@ -96,17 +106,9 @@ export const listVendors = queryWithRLS({ source: v.optional(v.array(v.string())), locale: v.optional(v.array(v.string())), sortField: v.optional(v.string()), - sortOrder: v.optional(v.union(v.literal('asc'), v.literal('desc'))), + sortOrder: v.optional(sortOrderValidator), }, - returns: v.object({ - items: v.array(v.any()), - total: v.number(), - page: v.number(), - pageSize: v.number(), - totalPages: v.number(), - hasNextPage: v.boolean(), - hasPreviousPage: v.boolean(), - }), + returns: vendorListResponseValidator, handler: async (ctx, args) => { const { page: currentPage, pageSize } = normalizePaginationOptions({ page: args.currentPage, @@ -208,23 +210,9 @@ export const updateVendor = mutationWithRLS({ email: v.optional(v.string()), phone: v.optional(v.string()), externalId: v.optional(v.string()), - source: v.optional( - v.union( - v.literal('manual_import'), - v.literal('file_upload'), - v.literal('circuly'), - ), - ), + source: v.optional(vendorSourceValidator), locale: v.optional(v.string()), - address: v.optional( - v.object({ - street: v.optional(v.string()), - city: v.optional(v.string()), - state: v.optional(v.string()), - country: v.optional(v.string()), - postalCode: v.optional(v.string()), - }), - ), + address: v.optional(vendorAddressValidator), tags: v.optional(v.array(v.string())), metadata: v.optional(v.any()), notes: v.optional(v.string()), @@ -309,33 +297,9 @@ export const deleteVendor = mutationWithRLS({ export const bulkCreateVendors = mutationWithRLS({ args: { organizationId: v.string(), - vendors: v.array( - v.object({ - name: v.optional(v.string()), - email: v.string(), - phone: v.optional(v.string()), - externalId: v.optional(v.string()), - source: v.union( - v.literal('manual_import'), - v.literal('file_upload'), - v.literal('circuly'), - ), - locale: v.optional(v.string()), - address: v.optional( - v.object({ - street: v.optional(v.string()), - city: v.optional(v.string()), - state: v.optional(v.string()), - country: v.optional(v.string()), - postalCode: v.optional(v.string()), - }), - ), - tags: v.optional(v.array(v.string())), - metadata: v.optional(v.any()), - notes: v.optional(v.string()), - }), - ), + vendors: v.array(vendorInputValidator), }, + returns: bulkCreateVendorsResponseValidator, handler: async (ctx, args) => { const results = { success: 0,