Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions apps/sim/lib/api/contracts/knowledge/documents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,16 +319,23 @@ export const listKnowledgeDocumentsContract = defineRouteContract({
},
})

export const createKnowledgeDocumentsContract = defineRouteContract({
method: 'POST',
path: '/api/knowledge/[id]/documents',
/**
* Document creation from inline content has no HTTP route: `POST
* /api/knowledge/[id]/documents` was retired when tool operations moved
* in-process, and the surviving `GET`/`PATCH` on that path would answer a `POST`
* with 405. So these stay plain schemas rather than a `defineRouteContract` —
* `lib/internal/knowledge/execute-tool.ts` validates `knowledge_create_document`
* against them directly. Callers wanting an HTTP upload use v1 or v2, both of
* which take multipart file bodies rather than inline content.
*/
export const createKnowledgeDocumentsSchemas = {
params: knowledgeBaseParamsSchema,
body: createKnowledgeDocumentsBodySchema,
response: {
mode: 'json',
schema: successResponseSchema(z.union([bulkCreateDocumentsResponseSchema, documentDataSchema])),
},
})
} as const

export const createKnowledgeDocumentsResponseSchema = successResponseSchema(
z.union([bulkCreateDocumentsResponseSchema, documentDataSchema])
)

export const updateKnowledgeDocumentContract = defineRouteContract({
method: 'PUT',
Expand Down
20 changes: 10 additions & 10 deletions apps/sim/lib/api/contracts/selectors/confluence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,14 +399,16 @@ export const confluencePageSelectorContract = definePostSelector(
z.object({ id: z.string(), title: z.string() }).passthrough()
)

export const confluenceUpdatePageContract = defineConfluencePutContract(
'/api/tools/confluence/page',
confluenceUpdatePageBodySchema
)
export const confluenceDeletePageContract = defineConfluenceDeleteContract(
'/api/tools/confluence/page',
confluenceDeletePageBodySchema
)
/**
* Page update and delete have no contract because they have no route: the
* `PUT`/`DELETE` handlers on `/api/tools/confluence/page` were retired when the
* tool moved in process, and the surviving selector `POST` on that path would
* answer either verb with 405. `lib/internal/confluence/execute-tool.ts`
* validates both against `confluenceUpdatePageBodySchema` /
* `confluenceDeletePageBodySchema` directly.
*/
export type ConfluenceUpdatePageBody = z.output<typeof confluenceUpdatePageBodySchema>
export type ConfluenceDeletePageBody = z.output<typeof confluenceDeletePageBodySchema>
export const confluenceDeleteAttachmentContract = defineConfluenceDeleteContract(
'/api/tools/confluence/attachment',
confluenceDeleteAttachmentBodySchema
Expand Down Expand Up @@ -562,8 +564,6 @@ export const confluenceUserContract = defineConfluencePostContract(

export type ConfluencePagesBody = ContractBody<typeof confluencePagesSelectorContract>
export type ConfluencePageBody = ContractBody<typeof confluencePageSelectorContract>
export type ConfluenceUpdatePageBody = ContractBody<typeof confluenceUpdatePageContract>
export type ConfluenceDeletePageBody = ContractBody<typeof confluenceDeletePageContract>
export type ConfluenceDeleteAttachmentBody = ContractBody<typeof confluenceDeleteAttachmentContract>
export type ConfluenceListAttachmentsQuery = ContractQuery<typeof confluenceListAttachmentsContract>
export type ConfluenceListBlogPostsQuery = ContractQuery<typeof confluenceListBlogPostsContract>
Expand Down
20 changes: 0 additions & 20 deletions apps/sim/lib/api/contracts/tools/docusign.ts

This file was deleted.

1 change: 0 additions & 1 deletion apps/sim/lib/api/contracts/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ export * from './communication'
export * from './crowdstrike'
export * from './custom'
export * from './databases'
export * from './docusign'
export * from './file'
export * from './google'
export * from './imap'
Expand Down
22 changes: 1 addition & 21 deletions apps/sim/lib/api/contracts/tools/media/document-parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { resolvedSecretTraceProvenanceSchema } from '@/lib/api/contracts/primiti
import { AWS_REGION_PATTERN, toolJsonResponseSchema } from '@/lib/api/contracts/tools/media/shared'
import { defineRouteContract } from '@/lib/api/contracts/types'
import { RESOLVED_SECRET_PROVENANCE_FIELD } from '@/lib/execution/private-tool-metadata'
import { FileInputSchema, RawFileInputSchema } from '@/lib/uploads/utils/file-schemas'
import { RawFileInputSchema } from '@/lib/uploads/utils/file-schemas'

const textractQuerySchema = z.object({
Text: z.string().min(1),
Expand Down Expand Up @@ -110,19 +110,6 @@ export const textractAnalyzeIdBodySchema = z
}
})

export const mistralParseBodySchema = z.object({
apiKey: z.string().min(1, 'API key is required'),
filePath: z.string().min(1, 'File path is required').optional(),
fileData: FileInputSchema.optional(),
file: FileInputSchema.optional(),
resultType: z.string().optional(),
pages: z.array(z.number()).optional(),
includeImageBase64: z.boolean().optional(),
imageLimit: z.number().optional(),
imageMinSize: z.number().optional(),
[RESOLVED_SECRET_PROVENANCE_FIELD]: resolvedSecretTraceProvenanceSchema.optional(),
})

export const textractParseContract = defineRouteContract({
method: 'POST',
path: '/api/tools/textract/parse',
Expand All @@ -143,10 +130,3 @@ export const textractAnalyzeIdContract = defineRouteContract({
body: textractAnalyzeIdBodySchema,
response: { mode: 'json', schema: toolJsonResponseSchema },
})

export const mistralParseContract = defineRouteContract({
method: 'POST',
path: '/api/tools/mistral/parse',
body: mistralParseBodySchema,
response: { mode: 'json', schema: toolJsonResponseSchema },
})
1 change: 0 additions & 1 deletion apps/sim/lib/api/contracts/tools/media/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export * from '@/lib/api/contracts/tools/media/document-parse'
export * from '@/lib/api/contracts/tools/media/image'
export * from '@/lib/api/contracts/tools/media/shared'
export * from '@/lib/api/contracts/tools/media/tts'
export * from '@/lib/api/contracts/tools/media/video'
92 changes: 0 additions & 92 deletions apps/sim/lib/api/contracts/tools/media/tts.ts

This file was deleted.

27 changes: 27 additions & 0 deletions apps/sim/lib/api/contracts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,33 @@ export type ResponseMode<S extends ApiSchema = ApiSchema> =
| StreamResponseMode
| RedirectResponseMode

/**
* A contract is consumed in one of two modes, and `method`/`path` only describe
* the first.
*
* **Boundary mode** — the common one. The contract bridges the client/server
* gap: a route builder under `app/api/**` serves `method` at `path`, and
* `requestJson(contract, …)` on the client parses the request out and validates
* the response back. Both sides read the same declaration, so `method` and
* `path` are load-bearing.
*
* **In-process mode.** Tool operations that once self-hopped over HTTP now
* execute in the same process (`lib/internal/<domain>/execute-tool.ts`), and
* they kept their contract as the input/response schema bundle —
* `parseInternalContractInput` reads only `params`, `query`, and `body`, and
* never looks at `method` or `path`. For these there is no route and no client
* fetch; `method` and `path` are vestigial, describing the HTTP endpoint the
* operation *used* to expose. Do not read them as evidence that an endpoint
* exists, and do not point a client at one.
*
* The distinction is not expressed in the type, so which mode a contract is in
* is derived, never annotated per file — `bun run check:api-contract-routes
* --list-in-process` enumerates the in-process set from the tree rather than
* from a hand-maintained list that would drift. That same audit enforces the
* part which actually matters: an in-process contract may not claim a `path`
* whose live route serves other methods, because a caller trusting the
* declaration gets a 405 rather than an honest 404.
*/
export interface ApiRouteContract<
TParams extends ApiSchema | undefined = undefined,
TQuery extends ApiSchema | undefined = undefined,
Expand Down
56 changes: 41 additions & 15 deletions apps/sim/lib/internal/confluence/execute-tool.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { getErrorMessage } from '@sim/utils/errors'
import type { AnyApiRouteContract, ContractBody, ContractQuery } from '@/lib/api/contracts'
import type {
AnyApiRouteContract,
ApiSchema,
ContractBody,
ContractQuery,
} from '@/lib/api/contracts'
import {
confluenceBlogPostOperationContract,
confluenceCreateCommentContract,
Expand All @@ -10,7 +15,7 @@ import {
confluenceDeleteBlogPostContract,
confluenceDeleteCommentContract,
confluenceDeleteLabelContract,
confluenceDeletePageContract,
confluenceDeletePageBodySchema,
confluenceDeletePagePropertyContract,
confluenceDeleteSpaceContract,
confluenceGetSpaceContract,
Expand All @@ -37,7 +42,7 @@ import {
confluenceTasksContract,
confluenceUpdateBlogPostContract,
confluenceUpdateCommentContract,
confluenceUpdatePageContract,
confluenceUpdatePageBodySchema,
confluenceUpdateSpaceContract,
confluenceUploadAttachmentContract,
confluenceUserContract,
Expand Down Expand Up @@ -94,12 +99,10 @@ import type {

type ContractInput<C extends AnyApiRouteContract> = NonNullable<ContractBody<C> | ContractQuery<C>>

function parsePreparedRequest<C extends AnyApiRouteContract>(
contract: C,
function parsePreparedInput<T>(
schema: ApiSchema,
request: InternalToolOperationCall
): { success: true; data: ContractInput<C> } | { success: false; response: Response } {
const schema = contract.query ?? contract.body
if (!schema) throw new Error(`Confluence contract ${contract.path} has no request input`)
): { success: true; data: T } | { success: false; response: Response } {
const parsed = schema.safeParse(request.input)
if (!parsed.success) {
return {
Expand All @@ -110,16 +113,21 @@ function parsePreparedRequest<C extends AnyApiRouteContract>(
),
}
}
return { success: true, data: parsed.data as ContractInput<C> }
return { success: true, data: parsed.data as T }
}

async function executeOperation<C extends AnyApiRouteContract>(
contract: C,
/**
* Operations whose HTTP route was retired hold a bare request schema rather than
* a contract, so they cannot declare a `method` and `path` nothing serves. The
* contract form below feeds this the schema it would have parsed anyway.
*/
async function executeSchemaOperation<T>(
schema: ApiSchema,
request: InternalToolOperationCall,
execute: (input: ContractInput<C>, context: ConfluenceOperationContext) => Promise<unknown>
execute: (input: T, context: ConfluenceOperationContext) => Promise<unknown>
): Promise<Response> {
request.signal?.throwIfAborted()
const parsed = parsePreparedRequest(contract, request)
const parsed = parsePreparedInput<T>(schema, request)
if (!parsed.success) return parsed.response
try {
const result = await execute(parsed.data, {
Expand All @@ -141,6 +149,16 @@ async function executeOperation<C extends AnyApiRouteContract>(
}
}

function executeOperation<C extends AnyApiRouteContract>(
contract: C,
request: InternalToolOperationCall,
execute: (input: ContractInput<C>, context: ConfluenceOperationContext) => Promise<unknown>
): Promise<Response> {
const schema = contract.query ?? contract.body
if (!schema) throw new Error(`Confluence contract ${contract.path} has no request input`)
return executeSchemaOperation<ContractInput<C>>(schema, request, execute)
}

export const executeConfluenceTool: InternalToolOperationHandler = async (request) => {
switch (request.toolId) {
case 'confluence_add_label':
Expand Down Expand Up @@ -194,7 +212,11 @@ export const executeConfluenceTool: InternalToolOperationHandler = async (reques
case 'confluence_delete_label':
return executeOperation(confluenceDeleteLabelContract, request, executeConfluenceDeleteLabel)
case 'confluence_delete_page':
return executeOperation(confluenceDeletePageContract, request, executeConfluenceDeletePage)
return executeSchemaOperation(
confluenceDeletePageBodySchema,
request,
executeConfluenceDeletePage
)
case 'confluence_delete_page_property':
return executeOperation(
confluenceDeletePagePropertyContract,
Expand Down Expand Up @@ -327,7 +349,11 @@ export const executeConfluenceTool: InternalToolOperationHandler = async (reques
executeConfluenceSearchInSpace
)
case 'confluence_update':
return executeOperation(confluenceUpdatePageContract, request, executeConfluenceUpdatePage)
return executeSchemaOperation(
confluenceUpdatePageBodySchema,
request,
executeConfluenceUpdatePage
)
case 'confluence_update_blogpost':
return executeOperation(
confluenceUpdateBlogPostContract,
Expand Down
Loading
Loading