From 1a7e773472018c3f006d56c158d32c0b67b6109a Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Mon, 31 Aug 2026 17:20:18 -0700 Subject: [PATCH 1/2] fix(confluence): normalize space identifiers --- apps/sim/blocks/blocks/confluence.test.ts | 82 +++++++++++ apps/sim/blocks/blocks/confluence.ts | 43 +++++- .../internal/confluence/operations.test.ts | 59 ++++++++ .../sim/lib/internal/confluence/operations.ts | 131 +++++++++++++----- .../migrations/subblock-migrations.ts | 8 ++ 5 files changed, 284 insertions(+), 39 deletions(-) create mode 100644 apps/sim/blocks/blocks/confluence.test.ts diff --git a/apps/sim/blocks/blocks/confluence.test.ts b/apps/sim/blocks/blocks/confluence.test.ts new file mode 100644 index 00000000000..021251f2f70 --- /dev/null +++ b/apps/sim/blocks/blocks/confluence.test.ts @@ -0,0 +1,82 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { ConfluenceV2Block } from '@/blocks/blocks/confluence' + +const { mockGetBlock } = vi.hoisted(() => ({ mockGetBlock: vi.fn() })) + +vi.mock('@/blocks/registry', () => ({ + getBlock: mockGetBlock, + getAllBlocks: vi.fn(() => []), + getLatestBlock: vi.fn(() => undefined), + getBlockRegistry: vi.fn(() => ({})), + getBlockByToolName: vi.fn(() => undefined), + getBlocksByCategory: vi.fn(() => []), +})) + +import { migrateSubblockIds } from '@/lib/workflows/migrations/subblock-migrations' +import { extractBlockParams } from '@/serializer' +import type { BlockState } from '@/stores/workflows/workflow/types' + +function legacySearchBlock(field: string, value: string, advancedMode: boolean): BlockState { + const values = { operation: 'search_in_space', [field]: value } + return { + id: 'block-1', + type: 'confluence_v2', + name: 'Confluence 1', + position: { x: 0, y: 0 }, + advancedMode, + subBlocks: Object.fromEntries( + Object.entries(values).map(([id, fieldValue]) => [ + id, + { id, type: 'short-input', value: fieldValue }, + ]) + ), + outputs: {}, + enabled: true, + } as unknown as BlockState +} + +function mappedSearchParams(state: BlockState): { + blocks: Record + params: Record +} { + const { blocks } = migrateSubblockIds({ 'block-1': state }) + const params = extractBlockParams(blocks['block-1']) + const transform = ConfluenceV2Block.tools.config?.params + if (!transform) throw new Error('Confluence V2 block has no params transform') + return { blocks, params: { ...params, ...transform(params) } } +} + +describe('Confluence search-in-space values saved before the selector split', () => { + beforeEach(() => { + vi.clearAllMocks() + mockGetBlock.mockReturnValue(ConfluenceV2Block) + }) + + it.each([ + { + mode: 'basic', + source: 'spaceSelector', + target: 'spaceKeySelector', + value: 'ENG', + advancedMode: false, + }, + { + mode: 'advanced', + source: 'spaceId', + target: 'manualSpaceKey', + value: '12345', + advancedMode: true, + }, + ])('migrates the $mode value and sends it as spaceKey', (testCase) => { + const { blocks, params } = mappedSearchParams( + legacySearchBlock(testCase.source, testCase.value, testCase.advancedMode) + ) + + expect(blocks['block-1'].subBlocks[testCase.target]?.value).toBe(testCase.value) + expect(params).toMatchObject({ operation: 'search_in_space', spaceKey: testCase.value }) + expect(params.spaceId).toBeUndefined() + }) +}) diff --git a/apps/sim/blocks/blocks/confluence.ts b/apps/sim/blocks/blocks/confluence.ts index fd6fee5b10d..c01e4fa717d 100644 --- a/apps/sim/blocks/blocks/confluence.ts +++ b/apps/sim/blocks/blocks/confluence.ts @@ -16,6 +16,9 @@ const PAGE_FIELD = ['pageId', 'manualPageId'] as const */ const SPACE_FIELD = ['spaceSelector', 'spaceId'] as const +/** Canonical basic/advanced pair for V1 operations that require a space key. */ +const SPACE_KEY_FIELD = ['spaceKeySelector', 'manualSpaceKey'] as const + /** Canonical upload/reference pair for an attachment's file. V2 only. */ const ATTACHMENT_FILE_FIELD = ['attachmentFileUpload', 'attachmentFileReference'] as const @@ -485,7 +488,7 @@ export const ConfluenceV2Block: BlockConfig = { { text: ', up to', field: 'limit', after: 'results' }, ], search_in_space: [ - { text: 'Search', field: SPACE_FIELD, core: true }, + { text: 'Search', field: SPACE_KEY_FIELD, core: true }, { text: 'for', field: 'query' }, ], list_blogposts: ['List blog posts', { text: ', up to', field: 'limit', after: 'results' }], @@ -834,7 +837,6 @@ export const ConfluenceV2Block: BlockConfig = { 'update_space', 'delete_space', 'list_pages_in_space', - 'search_in_space', 'create_blogpost', 'list_blogposts_in_space', 'list_space_labels', @@ -861,7 +863,6 @@ export const ConfluenceV2Block: BlockConfig = { 'update_space', 'delete_space', 'list_pages_in_space', - 'search_in_space', 'create_blogpost', 'list_blogposts_in_space', 'list_space_labels', @@ -872,6 +873,29 @@ export const ConfluenceV2Block: BlockConfig = { ], }, }, + { + id: 'spaceKeySelector', + title: 'Space', + type: 'project-selector', + canonicalParamId: 'selectedSpaceKey', + serviceId: 'confluence', + selectorKey: 'confluence.spaces', + placeholder: 'Select Confluence space', + dependsOn: ['credential', 'domain'], + mode: 'basic', + required: true, + condition: { field: 'operation', value: 'search_in_space' }, + }, + { + id: 'manualSpaceKey', + title: 'Space Key', + type: 'short-input', + canonicalParamId: 'selectedSpaceKey', + placeholder: 'Enter Confluence space key', + mode: 'advanced', + required: true, + condition: { field: 'operation', value: 'search_in_space' }, + }, { id: 'blogPostId', title: 'Blog Post ID', @@ -1461,6 +1485,7 @@ export const ConfluenceV2Block: BlockConfig = { taskAssignedTo, spaceName, spaceKey, + selectedSpaceKey, spaceDescription, spacePropertyKey, spacePropertyValue, @@ -1626,6 +1651,15 @@ export const ConfluenceV2Block: BlockConfig = { } } + if (operation === 'search_in_space') { + return { + credential: oauthCredential, + operation, + spaceKey: selectedSpaceKey, + ...rest, + } + } + if (operation === 'update_space') { return { credential: oauthCredential, @@ -1730,6 +1764,7 @@ export const ConfluenceV2Block: BlockConfig = { oauthCredential: { type: 'string', description: 'Confluence access token' }, pageId: { type: 'string', description: 'Page identifier' }, spaceId: { type: 'string', description: 'Space identifier' }, + selectedSpaceKey: { type: 'string', description: 'Selected space key' }, blogPostId: { type: 'string', description: 'Blog post identifier' }, versionNumber: { type: 'number', description: 'Page version number' }, accountId: { type: 'string', description: 'Atlassian account ID' }, @@ -1758,7 +1793,7 @@ export const ConfluenceV2Block: BlockConfig = { taskStatus: { type: 'string', description: 'Task status (complete or incomplete)' }, taskAssignedTo: { type: 'string', description: 'Filter tasks by assignee account ID' }, spaceName: { type: 'string', description: 'Space name for create/update' }, - spaceKey: { type: 'string', description: 'Space key for create' }, + spaceKey: { type: 'string', description: 'Space key for create or scoped search' }, spaceDescription: { type: 'string', description: 'Space description' }, spacePropertyKey: { type: 'string', description: 'Space property key' }, spacePropertyValue: { type: 'json', description: 'Space property value' }, diff --git a/apps/sim/lib/internal/confluence/operations.test.ts b/apps/sim/lib/internal/confluence/operations.test.ts index 460cf35682c..8a1c1d70b31 100644 --- a/apps/sim/lib/internal/confluence/operations.test.ts +++ b/apps/sim/lib/internal/confluence/operations.test.ts @@ -24,6 +24,8 @@ vi.mock('@/lib/uploads/utils/file-utils.server', () => ({ import { ConfluenceOperationError } from '@/lib/internal/confluence/errors' import { executeConfluenceListLabels, + executeConfluenceListPagesInSpace, + executeConfluenceSearchInSpace, executeConfluenceUploadAttachment, } from '@/lib/internal/confluence/operations' @@ -83,6 +85,63 @@ describe('Confluence operations', () => { expect(response.bodyUsed).toBe(true) }) + it.each([ + { selectedValue: 'ENG', expectedCalls: 2 }, + { selectedValue: '12345', expectedCalls: 1 }, + ])( + 'uses numeric space IDs for V2 requests when the selected value is $selectedValue', + async ({ selectedValue, expectedCalls }) => { + const fetchMock = vi.fn(async (request: string | URL | Request) => { + const url = String(request) + if (url.includes('/spaces?')) { + return Response.json({ + results: [{ id: '12345', key: 'ENG', name: 'Engineering', status: 'current' }], + }) + } + return Response.json({ results: [] }) + }) + vi.stubGlobal('fetch', fetchMock) + + await expect( + executeConfluenceListPagesInSpace( + { ...CONNECTION, spaceId: selectedValue, limit: 25 }, + { headers: new Headers(), requestId: 'request-1' } + ) + ).resolves.toEqual({ pages: [], nextCursor: null }) + + expect(fetchMock).toHaveBeenCalledTimes(expectedCalls) + const urls = fetchMock.mock.calls.map(([request]) => String(request)) + expect(urls.at(-1)).toContain('/spaces/12345/pages?limit=25') + if (selectedValue === 'ENG') { + expect(urls[0]).toContain('/spaces?keys=ENG&limit=1&status=current') + } + } + ) + + it('resolves a legacy numeric space value before constructing key-based CQL', async () => { + const fetchMock = vi.fn(async (request: string | URL | Request) => { + const url = String(request) + if (url.includes('/api/v2/spaces/12345')) { + return Response.json({ id: '12345', key: 'ENG', name: 'Engineering' }) + } + return Response.json({ results: [], totalSize: 0 }) + }) + vi.stubGlobal('fetch', fetchMock) + + await expect( + executeConfluenceSearchInSpace( + { ...CONNECTION, spaceKey: '12345', query: 'release notes', limit: 25 }, + { headers: new Headers(), requestId: 'request-1' } + ) + ).resolves.toEqual({ results: [], spaceKey: 'ENG', totalSize: 0 }) + + expect(fetchMock).toHaveBeenCalledTimes(2) + const searchUrl = String(fetchMock.mock.calls[1][0]) + expect(new URL(searchUrl).searchParams.get('cql')).toBe( + 'space = "ENG" AND text ~ "release notes"' + ) + }) + it('fails closed before downloading a stored file without an acting user', async () => { let caught: unknown try { diff --git a/apps/sim/lib/internal/confluence/operations.ts b/apps/sim/lib/internal/confluence/operations.ts index cb1cd4146a5..824f529b244 100644 --- a/apps/sim/lib/internal/confluence/operations.ts +++ b/apps/sim/lib/internal/confluence/operations.ts @@ -52,6 +52,7 @@ import { isPayloadSizeLimitError } from '@/lib/core/utils/stream-limits' import { asArray, asObject, + type ConfluenceClient, createConfluenceClient, type JsonObject, nested, @@ -122,6 +123,71 @@ function mappedPage(value: unknown): JsonObject { } } +const NUMERIC_SPACE_ID_PATTERN = /^[1-9][0-9]{0,19}$/ +const SPACE_STATUSES = ['current', 'archived'] as const + +function normalizedConfluenceSpaceKey(value: string): string { + const spaceKey = value.trim() + if (!spaceKey || spaceKey.length > 255 || spaceKey.includes('\0')) { + throw new ConfluenceOperationError('Invalid Confluence space key', 400) + } + return spaceKey +} + +async function findConfluenceSpaceByKey( + client: ConfluenceClient, + spaceKey: string, + signal?: AbortSignal +): Promise { + for (const status of SPACE_STATUSES) { + const query = new URLSearchParams({ keys: spaceKey, limit: '1', status }) + const data = await client.json(client.apiV2(`/spaces?${query}`), {}, signal) + const match = asArray(data.results) + .map(asObject) + .find((space) => space.key === spaceKey) + if (match) return match + } + return null +} + +async function resolveConfluenceSpaceId( + client: ConfluenceClient, + value: string, + signal?: AbortSignal +): Promise { + const spaceIdentifier = value.trim() + if (NUMERIC_SPACE_ID_PATTERN.test(spaceIdentifier)) return spaceIdentifier + + const spaceKey = normalizedConfluenceSpaceKey(spaceIdentifier) + const space = await findConfluenceSpaceByKey(client, spaceKey, signal) + const resolvedId = space?.id + if ( + (typeof resolvedId !== 'string' && typeof resolvedId !== 'number') || + !NUMERIC_SPACE_ID_PATTERN.test(String(resolvedId)) + ) { + throw new ConfluenceOperationError(`Confluence space key "${spaceKey}" was not found`, 404) + } + return String(resolvedId) +} + +async function resolveConfluenceSpaceKey( + client: ConfluenceClient, + value: string, + signal?: AbortSignal +): Promise { + const spaceIdentifier = normalizedConfluenceSpaceKey(value) + if (!NUMERIC_SPACE_ID_PATTERN.test(spaceIdentifier)) return spaceIdentifier + + const space = await client.json(client.apiV2(`/spaces/${spaceIdentifier}`), {}, signal) + if (typeof space.key !== 'string' || !space.key) { + throw new ConfluenceOperationError( + `Confluence space ID "${spaceIdentifier}" did not return a space key`, + 422 + ) + } + return space.key +} + export async function executeConfluenceRetrievePage( input: ConfluencePageBody, context: ConfluenceOperationContext @@ -208,17 +274,11 @@ export async function executeConfluenceCreatePage( input: ConfluenceCreatePageBody, context: ConfluenceOperationContext ) { - if (!/^\d+$/.test(String(input.spaceId))) { - throw new ConfluenceOperationError( - 'Invalid Space ID. The Space ID must be a numeric value, not the space key from the URL. Use the "list" operation to get all spaces with their numeric IDs.', - 400 - ) - } - assertId(input.spaceId, 'spaceId') - if (input.parentId) assertId(input.parentId, 'parentId') const client = await createConfluenceClient(input, context.signal) + const spaceId = await resolveConfluenceSpaceId(client, input.spaceId, context.signal) + if (input.parentId) assertId(input.parentId, 'parentId') const body: JsonObject = { - spaceId: input.spaceId, + spaceId, status: 'current', title: input.title, body: { representation: 'storage', value: input.content }, @@ -853,9 +913,9 @@ export async function executeConfluenceSearchInSpace( input: ConfluenceSearchInSpaceBody, context: ConfluenceOperationContext ) { - assertId(input.spaceKey, 'spaceKey') const client = await createConfluenceClient(input, context.signal) - let cql = `space = "${escapeCql(input.spaceKey)}"` + const spaceKey = await resolveConfluenceSpaceKey(client, input.spaceKey, context.signal) + let cql = `space = "${escapeCql(spaceKey)}"` if (input.query) cql += ` AND text ~ "${escapeCql(input.query)}"` if (input.contentType) cql += ` AND type = "${escapeCql(input.contentType)}"` const query = new URLSearchParams({ cql, limit: cappedLimit(input.limit) }) @@ -873,21 +933,21 @@ export async function executeConfluenceSearchInSpace( lastModified: result.lastModified ?? null, } }) - return { results, spaceKey: input.spaceKey, totalSize: data.totalSize ?? results.length } + return { results, spaceKey, totalSize: data.totalSize ?? results.length } } export async function executeConfluenceListBlogPostsInSpace( input: ConfluenceSpaceBlogPostsBody, context: ConfluenceOperationContext ) { - assertId(input.spaceId, 'spaceId') const client = await createConfluenceClient(input, context.signal) + const spaceId = await resolveConfluenceSpaceId(client, input.spaceId, context.signal) const query = new URLSearchParams({ limit: cappedLimit(input.limit) }) if (input.status) query.set('status', input.status) if (input.bodyFormat) query.set('body-format', input.bodyFormat) if (input.cursor) query.set('cursor', input.cursor) const data = await client.json( - client.apiV2(`/spaces/${input.spaceId}/blogposts?${query}`), + client.apiV2(`/spaces/${spaceId}/blogposts?${query}`), {}, context.signal ) @@ -914,14 +974,14 @@ export async function executeConfluenceListPagesInSpace( input: ConfluenceSpacePagesBody, context: ConfluenceOperationContext ) { - assertId(input.spaceId, 'spaceId') const client = await createConfluenceClient(input, context.signal) + const spaceId = await resolveConfluenceSpaceId(client, input.spaceId, context.signal) const query = new URLSearchParams({ limit: cappedLimit(input.limit) }) if (input.status) query.set('status', input.status) if (input.bodyFormat) query.set('body-format', input.bodyFormat) if (input.cursor) query.set('cursor', input.cursor) const data = await client.json( - client.apiV2(`/spaces/${input.spaceId}/pages?${query}`), + client.apiV2(`/spaces/${spaceId}/pages?${query}`), {}, context.signal ) @@ -938,12 +998,12 @@ export async function executeConfluenceListSpaceLabels( input: ConfluenceSpaceLabelsQuery, context: ConfluenceOperationContext ) { - assertId(input.spaceId, 'spaceId') const client = await createConfluenceClient(input, context.signal) + const spaceId = await resolveConfluenceSpaceId(client, input.spaceId, context.signal) const query = new URLSearchParams({ limit: cappedLimit(input.limit) }) if (input.cursor) query.set('cursor', input.cursor) const data = await client.json( - client.apiV2(`/spaces/${input.spaceId}/labels?${query}`), + client.apiV2(`/spaces/${spaceId}/labels?${query}`), {}, context.signal ) @@ -952,7 +1012,7 @@ export async function executeConfluenceListSpaceLabels( const label = asObject(value) return { id: label.id, name: label.name, prefix: label.prefix || 'global' } }), - spaceId: input.spaceId, + spaceId, nextCursor: nextCursor(data), } } @@ -961,13 +1021,13 @@ export async function executeConfluenceListSpacePermissions( input: ConfluenceSpacePermissionsBody, context: ConfluenceOperationContext ) { - assertId(input.spaceId, 'spaceId') assertCursor(input.cursor) const client = await createConfluenceClient(input, context.signal) + const spaceId = await resolveConfluenceSpaceId(client, input.spaceId, context.signal) const query = new URLSearchParams({ limit: cappedLimit(input.limit) }) if (input.cursor) query.set('cursor', input.cursor) const data = await client.json( - client.apiV2(`/spaces/${input.spaceId}/permissions?${query}`), + client.apiV2(`/spaces/${spaceId}/permissions?${query}`), {}, context.signal ) @@ -984,7 +1044,7 @@ export async function executeConfluenceListSpacePermissions( unlicensedAccess: permission.unlicensedAccess ?? false, } }), - spaceId: input.spaceId, + spaceId, nextCursor: nextCursor(data), } } @@ -1025,10 +1085,11 @@ export async function executeConfluenceCreateBlogPost( throw new ConfluenceOperationError('Invalid create blog post request', 400) } const client = await createConfluenceClient(input, context.signal) + const spaceId = await resolveConfluenceSpaceId(client, input.spaceId, context.signal) const data = await client.json( client.apiV2('/blogposts'), jsonInit('POST', { - spaceId: input.spaceId, + spaceId, status: input.status || 'current', title: input.title, body: { representation: 'storage', value: input.content }, @@ -1123,9 +1184,9 @@ export async function executeConfluenceGetSpace( input: ConfluenceGetSpaceQuery, context: ConfluenceOperationContext ) { - assertId(input.spaceId, 'spaceId') const client = await createConfluenceClient(input, context.signal) - return client.json(client.apiV2(`/spaces/${input.spaceId}`), {}, context.signal) + const spaceId = await resolveConfluenceSpaceId(client, input.spaceId, context.signal) + return client.json(client.apiV2(`/spaces/${spaceId}`), {}, context.signal) } export async function executeConfluenceCreateSpace( @@ -1144,7 +1205,6 @@ export async function executeConfluenceUpdateSpace( input: ConfluenceUpdateSpaceBody, context: ConfluenceOperationContext ) { - assertId(input.spaceId, 'spaceId') if (!input.name && input.description === undefined) { throw new ConfluenceOperationError( 'At least one of name or description is required for update', @@ -1152,7 +1212,8 @@ export async function executeConfluenceUpdateSpace( ) } const client = await createConfluenceClient(input, context.signal) - const current = await client.json(client.apiV2(`/spaces/${input.spaceId}`), {}, context.signal) + const spaceId = await resolveConfluenceSpaceId(client, input.spaceId, context.signal) + const current = await client.json(client.apiV2(`/spaces/${spaceId}`), {}, context.signal) const body: JsonObject = { name: input.name || current.name } if (input.description !== undefined) { body.description = { plain: { value: input.description, representation: 'plain' } } @@ -1168,9 +1229,9 @@ export async function executeConfluenceDeleteSpace( input: ConfluenceDeleteSpaceBody, context: ConfluenceOperationContext ) { - assertId(input.spaceId, 'spaceId') const client = await createConfluenceClient(input, context.signal) - const current = await client.json(client.apiV2(`/spaces/${input.spaceId}`), {}, context.signal) + const spaceId = await resolveConfluenceSpaceId(client, input.spaceId, context.signal) + const current = await client.json(client.apiV2(`/spaces/${spaceId}`), {}, context.signal) const response = await client.fetch( client.rest(`/space/${encodeURIComponent(String(current.key))}`), { method: 'DELETE' }, @@ -1190,7 +1251,7 @@ export async function executeConfluenceDeleteSpace( context.signal?.throwIfAborted() } return { - spaceId: input.spaceId, + spaceId, deleted: true, longTaskId: longTask.id, longTaskStatusLink: nested(longTask, 'links', 'status'), @@ -1228,16 +1289,16 @@ export async function executeConfluenceSpaceProperties( input: ConfluenceSpacePropertiesBody, context: ConfluenceOperationContext ) { - assertId(input.spaceId, 'spaceId') const client = await createConfluenceClient(input, context.signal) - const base = client.apiV2(`/spaces/${input.spaceId}/properties`) + const spaceId = await resolveConfluenceSpaceId(client, input.spaceId, context.signal) + const base = client.apiV2(`/spaces/${spaceId}/properties`) if (input.action === 'delete') { if (!input.propertyId) { throw new ConfluenceOperationError('Property ID is required for delete action', 400) } assertId(input.propertyId, 'propertyId') await client.delete(`${base}/${encodeURIComponent(input.propertyId)}`, context.signal) - return { spaceId: input.spaceId, propertyId: input.propertyId, deleted: true } + return { spaceId, propertyId: input.propertyId, deleted: true } } if (input.action === 'create') { if (!input.key) { @@ -1248,7 +1309,7 @@ export async function executeConfluenceSpaceProperties( jsonInit('POST', { key: input.key, value: input.value ?? {} }), context.signal ) - return { propertyId: data.id, key: data.key, value: data.value ?? null, spaceId: input.spaceId } + return { propertyId: data.id, key: data.key, value: data.value ?? null, spaceId } } assertCursor(input.cursor) const query = new URLSearchParams({ limit: cappedLimit(input.limit) }) @@ -1259,7 +1320,7 @@ export async function executeConfluenceSpaceProperties( const property = asObject(value) return { id: property.id, key: property.key, value: property.value ?? null } }), - spaceId: input.spaceId, + spaceId, nextCursor: nextCursor(data), } } diff --git a/apps/sim/lib/workflows/migrations/subblock-migrations.ts b/apps/sim/lib/workflows/migrations/subblock-migrations.ts index 52f91909997..a0df4dcce38 100644 --- a/apps/sim/lib/workflows/migrations/subblock-migrations.ts +++ b/apps/sim/lib/workflows/migrations/subblock-migrations.ts @@ -140,6 +140,14 @@ export const SUBBLOCK_ID_MIGRATIONS: Record Date: Mon, 31 Aug 2026 17:33:54 -0700 Subject: [PATCH 2/2] fix(confluence): hydrate legacy numeric space selections --- .../server/providers/confluence.test.ts | 19 +++++++++++++++++++ .../selectors/server/providers/confluence.ts | 7 +++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/apps/sim/lib/selectors/server/providers/confluence.test.ts b/apps/sim/lib/selectors/server/providers/confluence.test.ts index cfbbbd520be..07e6d87ec37 100644 --- a/apps/sim/lib/selectors/server/providers/confluence.test.ts +++ b/apps/sim/lib/selectors/server/providers/confluence.test.ts @@ -127,6 +127,25 @@ describe('Confluence server selector adapters', () => { expect(String(mockFetch.mock.calls[0]?.[0])).toContain('/wiki/api/v2/spaces/12345') }) + it('hydrates a legacy numeric value in the key selector without rewriting it', async () => { + mockFetch.mockResolvedValueOnce( + new Response(JSON.stringify({ id: '12345', key: 'ENG', name: 'Engineering' }), { + status: 200, + }) + ) + + await expect( + confluenceSelectorAttachments['confluence.spaces'].execute({ + ...spaceDetailArgs(), + request: { kind: 'detail', id: '12345' }, + }) + ).resolves.toEqual({ + kind: 'detail', + item: { id: '12345', label: 'Engineering (ENG)' }, + }) + expect(String(mockFetch.mock.calls[0]?.[0])).toContain('/wiki/api/v2/spaces/12345') + }) + it('projects provider IDs for block space lists while key selectors remain unchanged', async () => { mockFetch .mockResolvedValueOnce( diff --git a/apps/sim/lib/selectors/server/providers/confluence.ts b/apps/sim/lib/selectors/server/providers/confluence.ts index 7da7071b9a3..31a25f8dc58 100644 --- a/apps/sim/lib/selectors/server/providers/confluence.ts +++ b/apps/sim/lib/selectors/server/providers/confluence.ts @@ -124,7 +124,7 @@ async function executeSpaces(args: ExecuteServerSelectorArgs, identifier: 'key' if (args.request.kind === 'detail') { const requestedId = args.request.id.trim() if (!requestedId || requestedId.length > 255) throw new SelectorContextUnavailableError() - if (identifier === 'id' && /^[1-9][0-9]{0,19}$/.test(requestedId)) { + if (/^[1-9][0-9]{0,19}$/.test(requestedId)) { const space = await fetchProviderJson( `https://api.atlassian.com/ex/confluence/${auth.cloudId}/wiki/api/v2/spaces/${requestedId}`, { @@ -133,7 +133,10 @@ async function executeSpaces(args: ExecuteServerSelectorArgs, identifier: 'key' } ) if (!space.id || !space.key || !space.name) throw new SelectorOptionsUnavailableError() - return detailSelectorResult(spaceOption(space, space.status ?? 'current', 'id')) + return detailSelectorResult({ + ...spaceOption(space, space.status ?? 'current', identifier), + id: requestedId, + }) } const key = requestedId