mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 05:29:44 +02:00
chore: Move the deprecated GET /workflows/{id}/{versionId} alias to the decorator controller and delete the legacy workflows handler (#40340)
Co-authored-by: n8n-cat-bot[bot] <n8n-cat-bot[bot]@users.noreply.github.com> Co-authored-by: Sam Wooler <swooler592@gmail.com> Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e5c8a8bb62
commit
0bd0cd2873
|
|
@ -526,7 +526,9 @@ export {
|
|||
workflowVersionListItemPublicSchema,
|
||||
} from './workflow-history/workflow-version-history-public.dto';
|
||||
export {
|
||||
DeprecatedWorkflowVersionPublicDto,
|
||||
WorkflowVersionPublicDto,
|
||||
deprecatedWorkflowVersionPublicSchema,
|
||||
workflowVersionPublicSchema,
|
||||
} from './workflow-history/workflow-version-public.dto';
|
||||
|
||||
|
|
|
|||
|
|
@ -7,3 +7,15 @@ export const workflowVersionPublicSchema = activeWorkflowVersionPublicSchema.omi
|
|||
});
|
||||
|
||||
export class WorkflowVersionPublicDto extends Z.class(workflowVersionPublicSchema.shape) {}
|
||||
|
||||
export const deprecatedWorkflowVersionPublicSchema = workflowVersionPublicSchema.partial({
|
||||
nodeGroups: true,
|
||||
name: true,
|
||||
description: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
});
|
||||
|
||||
export class DeprecatedWorkflowVersionPublicDto extends Z.class(
|
||||
deprecatedWorkflowVersionPublicSchema.shape,
|
||||
) {}
|
||||
|
|
|
|||
|
|
@ -163,7 +163,6 @@ export default defineConfig(
|
|||
files: [
|
||||
'./src/public-api/v1/handlers/log-streaming/log-streaming.handler.ts',
|
||||
'./src/public-api/v1/handlers/n8n-packages/n8n-packages.handler.ts',
|
||||
'./src/public-api/v1/handlers/workflows/workflows.handler.ts',
|
||||
],
|
||||
rules: {
|
||||
'n8n-local-rules/require-public-api-controller': 'off',
|
||||
|
|
|
|||
|
|
@ -157,7 +157,6 @@ export default defineConfig({
|
|||
'./src/public-api/v1/handlers/evaluations/evaluations.handler.ts',
|
||||
'./src/public-api/v1/handlers/log-streaming/log-streaming.handler.ts',
|
||||
'./src/public-api/v1/handlers/n8n-packages/n8n-packages.handler.ts',
|
||||
'./src/public-api/v1/handlers/workflows/workflows.handler.ts',
|
||||
],
|
||||
rules: {
|
||||
'n8n-local-rules/require-public-api-controller': 'off',
|
||||
|
|
|
|||
|
|
@ -19,16 +19,6 @@ export type PaginatedRequest = AuthenticatedRequest<
|
|||
lastId?: string;
|
||||
}
|
||||
>;
|
||||
export declare namespace WorkflowRequest {
|
||||
type Activate = AuthenticatedRequest<
|
||||
{ id: string },
|
||||
{},
|
||||
{ versionId?: string; name?: string; description?: string },
|
||||
{}
|
||||
>;
|
||||
type GetVersion = AuthenticatedRequest<{ id: string; versionId: string }, {}, {}, {}>;
|
||||
}
|
||||
|
||||
export declare namespace PackageRequest {
|
||||
type Import = AuthenticatedRequest<
|
||||
{},
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
CreatedWorkflowPublicDto,
|
||||
CreateWorkflowPublicDto,
|
||||
DeletedWorkflowPublicDto,
|
||||
DeprecatedWorkflowVersionPublicDto,
|
||||
GetWorkflowQueryDto,
|
||||
ListWorkflowHistoryQueryDto,
|
||||
ListWorkflowsQueryDto,
|
||||
|
|
@ -75,7 +76,8 @@ import { WorkflowHistoryService } from '@/workflows/workflow-history/workflow-hi
|
|||
import { WorkflowService } from '@/workflows/workflow.service';
|
||||
import { EnterpriseWorkflowService } from '@/workflows/workflow.service.ee';
|
||||
|
||||
const DEPRECATED_ALIAS_SINCE = new Date('2026-07-23T00:00:00Z');
|
||||
const ACTIVATE_DEACTIVATE_DEPRECATED_SINCE = new Date('2026-07-23T00:00:00Z');
|
||||
const OLD_VERSION_PATH_DEPRECATED_SINCE = new Date('2026-08-26T00:00:00Z');
|
||||
|
||||
const UPDATE_CONFLICT_DESCRIPTION =
|
||||
'Conflict, e.g. re-publication blocked by an open workflow review (then `reason` and ' +
|
||||
|
|
@ -650,7 +652,7 @@ export class WorkflowsPublicController {
|
|||
}
|
||||
|
||||
@Post('/:workflowId/activate')
|
||||
@Deprecated({ since: DEPRECATED_ALIAS_SINCE })
|
||||
@Deprecated({ since: ACTIVATE_DEACTIVATE_DEPRECATED_SINCE })
|
||||
@ApiKeyScope('workflow:activate')
|
||||
@ProjectScope('workflow:publish')
|
||||
@ApiSummary('Publish a workflow')
|
||||
|
|
@ -675,7 +677,7 @@ export class WorkflowsPublicController {
|
|||
}
|
||||
|
||||
@Post('/:workflowId/deactivate')
|
||||
@Deprecated({ since: DEPRECATED_ALIAS_SINCE })
|
||||
@Deprecated({ since: ACTIVATE_DEACTIVATE_DEPRECATED_SINCE })
|
||||
@ApiKeyScope('workflow:deactivate')
|
||||
@ProjectScope('workflow:unpublish')
|
||||
@ApiSummary('Deactivate a workflow')
|
||||
|
|
@ -831,4 +833,44 @@ export class WorkflowsPublicController {
|
|||
|
||||
return tags.map(toPublicTag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep this route as the last registered route in the controller, so that it does not shadow other routes.
|
||||
*/
|
||||
@Get('/:workflowId/:workflowVersionId')
|
||||
@Deprecated({ since: OLD_VERSION_PATH_DEPRECATED_SINCE })
|
||||
@ApiKeyScope('workflow:read')
|
||||
@ProjectScope('workflow:read')
|
||||
@ApiSummary('Retrieves a specific version of a workflow')
|
||||
@ApiDescription(
|
||||
'Deprecated: use GET /workflows/{workflowId}/versions/{workflowVersionId} instead. ' +
|
||||
'Retrieves a specific version of a workflow from workflow history.',
|
||||
)
|
||||
@ApiTags(['Workflow'])
|
||||
@ApiResponse(200, DeprecatedWorkflowVersionPublicDto)
|
||||
@ApiErrorResponse(404)
|
||||
async getDeprecatedWorkflowVersion(
|
||||
req: AuthenticatedRequest,
|
||||
_res: Response,
|
||||
@Param('workflowId', workflowIdParamSchema) workflowId: string,
|
||||
@Param('workflowVersionId', workflowVersionIdParamSchema) workflowVersionId: string,
|
||||
): Promise<DeprecatedWorkflowVersionPublicDto> {
|
||||
try {
|
||||
const version = await this.workflowHistoryService.getVersion(
|
||||
req.user,
|
||||
workflowId,
|
||||
workflowVersionId,
|
||||
{ includePublishHistory: false },
|
||||
);
|
||||
|
||||
this.eventService.emit('user-retrieved-workflow-version', {
|
||||
userId: req.user.id,
|
||||
publicApi: true,
|
||||
});
|
||||
|
||||
return toPublicWorkflowVersion(version);
|
||||
} catch (error) {
|
||||
throw new NotFoundError('Version not found');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,181 @@
|
|||
operationId: getDeprecatedWorkflowVersion
|
||||
tags:
|
||||
- Workflow
|
||||
summary: Retrieves a specific version of a workflow
|
||||
description: 'Deprecated: use GET /workflows/{workflowId}/versions/{workflowVersionId} instead. Retrieves a specific version of a workflow from workflow history.'
|
||||
x-required-scope: workflow:read
|
||||
deprecated: true
|
||||
x-eov-operation-id: unreachable
|
||||
x-eov-operation-handler: v1/handlers/decorator-routed.handler
|
||||
x-decorator-routed: true
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
required: true
|
||||
description: The ID of the workflow.
|
||||
name: workflowId
|
||||
in: path
|
||||
- schema:
|
||||
type: string
|
||||
required: true
|
||||
description: The ID of the workflow version.
|
||||
name: workflowVersionId
|
||||
in: path
|
||||
responses:
|
||||
'200':
|
||||
description: Operation successful.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
versionId:
|
||||
type: string
|
||||
workflowId:
|
||||
type: string
|
||||
nodes:
|
||||
type: array
|
||||
description: Nodes that make up the workflow
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
example: 0f5532f9-36ba-4bef-86c7-30d607400b15
|
||||
name:
|
||||
type: string
|
||||
example: Jira
|
||||
webhookId:
|
||||
type: string
|
||||
disabled:
|
||||
type: boolean
|
||||
notesInFlow:
|
||||
type: boolean
|
||||
notes:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
example: n8n-nodes-base.jira
|
||||
typeVersion:
|
||||
type: number
|
||||
example: 1
|
||||
executeOnce:
|
||||
type: boolean
|
||||
example: false
|
||||
alwaysOutputData:
|
||||
type: boolean
|
||||
example: false
|
||||
retryOnFail:
|
||||
type: boolean
|
||||
example: false
|
||||
maxTries:
|
||||
type: number
|
||||
waitBetweenTries:
|
||||
type: number
|
||||
continueOnFail:
|
||||
type: boolean
|
||||
example: false
|
||||
description: use onError instead
|
||||
deprecated: true
|
||||
onError:
|
||||
type: string
|
||||
example: stopWorkflow
|
||||
position:
|
||||
type: array
|
||||
items:
|
||||
type: number
|
||||
example:
|
||||
- -100
|
||||
- 80
|
||||
parameters:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
credentials:
|
||||
type: object
|
||||
example:
|
||||
jiraSoftwareCloudApi:
|
||||
name: jiraApi
|
||||
customTelemetryTags:
|
||||
type: object
|
||||
properties:
|
||||
tag:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
required:
|
||||
- key
|
||||
- value
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
connections:
|
||||
type: object
|
||||
description: Connections between nodes, keyed by source node name
|
||||
example:
|
||||
Jira:
|
||||
main:
|
||||
- - node: Jira
|
||||
type: main
|
||||
index: 0
|
||||
nodeGroups:
|
||||
type: array
|
||||
description: Visual groupings of nodes shown as frames on the canvas
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Unique identifier for the node group
|
||||
example: 9b1c8e2a-4d3f-4a6b-8c7d-1e2f3a4b5c6d
|
||||
name:
|
||||
type: string
|
||||
description: Display name of the node group
|
||||
example: Data processing
|
||||
description:
|
||||
type: string
|
||||
maxLength: 155
|
||||
description: Optional plain-text description of the node group
|
||||
example: Cleans and normalizes incoming records
|
||||
nodeIds:
|
||||
type: array
|
||||
description: IDs of the nodes that belong to this group
|
||||
items:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- nodeIds
|
||||
authors:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
nullable: true
|
||||
description:
|
||||
type: string
|
||||
nullable: true
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
required:
|
||||
- versionId
|
||||
- workflowId
|
||||
- nodes
|
||||
- connections
|
||||
- authors
|
||||
'401':
|
||||
$ref: ../../../../shared/spec/responses/unauthorized.yml
|
||||
'403':
|
||||
$ref: ../../../../shared/spec/responses/forbidden.yml
|
||||
'404':
|
||||
$ref: ../../../../shared/spec/responses/notFound.yml
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
get:
|
||||
x-eov-operation-id: getWorkflowVersion
|
||||
x-required-scope: workflow:read
|
||||
x-eov-operation-handler: v1/handlers/workflows/workflows.handler
|
||||
tags:
|
||||
- Workflow
|
||||
summary: Retrieves a specific version of a workflow
|
||||
description: >-
|
||||
Deprecated: use GET /workflows/{workflowId}/versions/{workflowVersionId}
|
||||
instead.
|
||||
Retrieves a specific version of a workflow from workflow history.
|
||||
deprecated: true
|
||||
parameters:
|
||||
- $ref: '../schemas/parameters/workflowId.yml'
|
||||
- name: versionId
|
||||
in: path
|
||||
required: true
|
||||
description: The version ID to retrieve
|
||||
schema:
|
||||
type: string
|
||||
example: 'abc123-def456-ghi789'
|
||||
responses:
|
||||
'200':
|
||||
description: Operation successful.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../schemas/workflowVersion.yml'
|
||||
'401':
|
||||
$ref: '../../../../shared/spec/responses/unauthorized.yml'
|
||||
'404':
|
||||
$ref: '../../../../shared/spec/responses/notFound.yml'
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- versionId
|
||||
- workflowId
|
||||
- nodes
|
||||
- connections
|
||||
- authors
|
||||
properties:
|
||||
versionId:
|
||||
type: string
|
||||
readOnly: true
|
||||
description: The version ID of this workflow snapshot
|
||||
example: abc123-def456
|
||||
workflowId:
|
||||
type: string
|
||||
readOnly: true
|
||||
description: The workflow ID this version belongs to
|
||||
example: 2tUt1wbLX592XDdX
|
||||
nodes:
|
||||
type: array
|
||||
items:
|
||||
$ref: './node.yml'
|
||||
description: Nodes as they were in this version
|
||||
readOnly: true
|
||||
connections:
|
||||
type: object
|
||||
example: { Jira: { main: [[{ node: 'Jira', type: 'main', index: 0 }]] } }
|
||||
description: Connections as they were in this version
|
||||
readOnly: true
|
||||
nodeGroups:
|
||||
type: array
|
||||
readOnly: true
|
||||
description: Node groups as they were in this version
|
||||
items:
|
||||
$ref: './workflowNodeGroup.yml'
|
||||
authors:
|
||||
type: string
|
||||
readOnly: true
|
||||
description: Authors who created this version
|
||||
example: John Doe
|
||||
name:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Workflow name at this version
|
||||
example: Workflow 1
|
||||
description:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Workflow description at this version
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
description: When this version was created
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
description: When this version was last updated
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
import { EventService } from '@n8n/backend-services';
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
import { NotFoundError } from '@n8n/errors';
|
||||
import { WorkflowHistoryService } from '@/workflows/workflow-history/workflow-history.service';
|
||||
|
||||
import type { WorkflowRequest } from '../../../types';
|
||||
import type { PublicAPIEndpoint } from '../../shared/handler.types';
|
||||
import {
|
||||
publicApiScope,
|
||||
projectScope,
|
||||
deprecated,
|
||||
} from '../../shared/middlewares/global.middleware';
|
||||
|
||||
const VERSION_PATH_DEPRECATED_SINCE = new Date('2026-08-26T00:00:00Z');
|
||||
|
||||
type WorkflowHandlers = {
|
||||
getWorkflowVersion: PublicAPIEndpoint<WorkflowRequest.GetVersion>;
|
||||
};
|
||||
|
||||
const workflowHandlers: WorkflowHandlers = {
|
||||
getWorkflowVersion: [
|
||||
deprecated({ since: VERSION_PATH_DEPRECATED_SINCE }),
|
||||
publicApiScope('workflow:read'),
|
||||
projectScope('workflow:read', 'workflow'),
|
||||
async (req, res) => {
|
||||
const { id: workflowId, versionId } = req.params;
|
||||
|
||||
try {
|
||||
const version = await Container.get(WorkflowHistoryService).getVersion(
|
||||
req.user,
|
||||
workflowId,
|
||||
versionId,
|
||||
{ includePublishHistory: false },
|
||||
);
|
||||
|
||||
Container.get(EventService).emit('user-retrieved-workflow-version', {
|
||||
userId: req.user.id,
|
||||
publicApi: true,
|
||||
});
|
||||
|
||||
const { autosaved, ...versionWithoutInternalFields } = version;
|
||||
|
||||
return res.json(versionWithoutInternalFields);
|
||||
} catch {
|
||||
throw new NotFoundError('Version not found');
|
||||
}
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export = workflowHandlers;
|
||||
|
|
@ -417,3 +417,6 @@ paths:
|
|||
$ref: ./handlers/workflows/spec/paths/getWorkflowTags.generated.yml
|
||||
put:
|
||||
$ref: ./handlers/workflows/spec/paths/updateWorkflowTags.generated.yml
|
||||
/workflows/{workflowId}/{workflowVersionId}:
|
||||
get:
|
||||
$ref: ./handlers/workflows/spec/paths/getDeprecatedWorkflowVersion.generated.yml
|
||||
|
|
|
|||
|
|
@ -79,8 +79,6 @@ tags:
|
|||
description: Operations about workflows
|
||||
|
||||
paths:
|
||||
/workflows/{id}/{versionId}:
|
||||
$ref: './handlers/workflows/spec/paths/workflows.id.versionId.yml'
|
||||
/n8n-packages/export:
|
||||
$ref: './handlers/n8n-packages/spec/paths/n8n-packages.export.yml'
|
||||
/n8n-packages/import:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,229 @@
|
|||
import { EventService } from '@n8n/backend-services';
|
||||
import { createWorkflow, mockInstance, testDb } from '@n8n/backend-test-utils';
|
||||
import type { User } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { Telemetry } from '@/telemetry';
|
||||
|
||||
import { createTestRun } from '../shared/db/evaluation';
|
||||
import { createTag } from '../shared/db/tags';
|
||||
import { createOwnerWithApiKey } from '../shared/db/users';
|
||||
import { createWorkflowHistoryItem } from '../shared/db/workflow-history';
|
||||
import type { SuperAgentTest } from '../shared/types';
|
||||
import * as utils from '../shared/utils/';
|
||||
|
||||
/**
|
||||
* `GET /workflows/:workflowId/:workflowVersionId` matches every two-segment GET path under
|
||||
* `/workflows`. Express answers with the first matching route, so the order of registration decides
|
||||
* which handler gets a request. These tests call the real app with seeded data and check the body
|
||||
* of each response, so a request that reaches the wrong handler fails the assertion.
|
||||
*
|
||||
* Once `GET /workflows/:workflowId/:workflowVersionId` is removed, these tests can be deleted.
|
||||
*/
|
||||
mockInstance(Telemetry);
|
||||
|
||||
let owner: User;
|
||||
let authOwnerAgent: SuperAgentTest;
|
||||
|
||||
const testServer = utils.setupTestServer({ endpointGroups: ['publicApi'] });
|
||||
|
||||
beforeAll(async () => {
|
||||
owner = await createOwnerWithApiKey();
|
||||
authOwnerAgent = testServer.publicApiAgentFor(owner);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate([
|
||||
'WorkflowEntity',
|
||||
'SharedWorkflow',
|
||||
'WorkflowHistory',
|
||||
'TagEntity',
|
||||
'TestRun',
|
||||
'TestCaseExecution',
|
||||
]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
/** A workflow with three saved versions, two tags and two test runs. */
|
||||
async function seedWorkflow(label: string, tagNames: string[]) {
|
||||
const workflow = await createWorkflow({}, owner);
|
||||
|
||||
const versions = [];
|
||||
for (const [index, name] of [`${label} v1`, `${label} v2`, `${label} v3`].entries()) {
|
||||
const workflowVersion = await createWorkflowHistoryItem(workflow.id, {
|
||||
versionId: uuid(),
|
||||
name,
|
||||
description: `${name} description`,
|
||||
authors: 'Test Author',
|
||||
createdAt: new Date(Date.now() + index),
|
||||
updatedAt: new Date(Date.now() + index),
|
||||
});
|
||||
versions.push(workflowVersion);
|
||||
}
|
||||
|
||||
const tags = [];
|
||||
for (const name of tagNames) {
|
||||
const tag = await createTag({ name }, workflow);
|
||||
tags.push(tag);
|
||||
}
|
||||
|
||||
const testRuns = [
|
||||
await createTestRun(workflow.id, { status: 'completed' }),
|
||||
await createTestRun(workflow.id, { status: 'error' }),
|
||||
];
|
||||
|
||||
return { workflow, versions, tags, testRuns };
|
||||
}
|
||||
|
||||
async function get(path: string) {
|
||||
const emit = vi.spyOn(Container.get(EventService), 'emit');
|
||||
|
||||
const response = await authOwnerAgent.get(path);
|
||||
|
||||
expect(emit.mock.calls.filter(([event]) => event === 'public-api-invoked')).toHaveLength(1);
|
||||
emit.mockRestore();
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
describe('workflow routes next to the deprecated version alias', () => {
|
||||
let target: Awaited<ReturnType<typeof seedWorkflow>>;
|
||||
|
||||
beforeEach(async () => {
|
||||
target = await seedWorkflow('Target', ['production', 'team-a']);
|
||||
});
|
||||
|
||||
test('GET /workflows/:workflowId/history returns the version list of that workflow', async () => {
|
||||
const response = await get(`/workflows/${target.workflow.id}/history`);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body).toStrictEqual({
|
||||
// Newest first version first.
|
||||
data: [...target.versions].reverse().map((version) => ({
|
||||
versionId: version.versionId,
|
||||
workflowId: target.workflow.id,
|
||||
authors: 'Test Author',
|
||||
name: version.name,
|
||||
description: version.description,
|
||||
createdAt: version.createdAt.toISOString(),
|
||||
updatedAt: version.updatedAt.toISOString(),
|
||||
})),
|
||||
nextCursor: null,
|
||||
});
|
||||
});
|
||||
|
||||
test('GET /workflows/:workflowId/tags returns the tags of that workflow', async () => {
|
||||
const response = await get(`/workflows/${target.workflow.id}/tags`);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
const [production, teamA] = target.tags;
|
||||
// The tags share one creation time, so the order is not defined. Sort by name.
|
||||
expect(
|
||||
response.body.sort((a: { name: string }, b: { name: string }) =>
|
||||
a.name.localeCompare(b.name),
|
||||
),
|
||||
).toStrictEqual([
|
||||
{
|
||||
id: production.id,
|
||||
name: 'production',
|
||||
createdAt: production.createdAt.toISOString(),
|
||||
updatedAt: production.updatedAt.toISOString(),
|
||||
},
|
||||
{
|
||||
id: teamA.id,
|
||||
name: 'team-a',
|
||||
createdAt: teamA.createdAt.toISOString(),
|
||||
updatedAt: teamA.updatedAt.toISOString(),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('GET /workflows/:workflowId/test-runs returns the test runs of that workflow', async () => {
|
||||
const response = await get(`/workflows/${target.workflow.id}/test-runs`);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
const [completedRun, errorRun] = target.testRuns;
|
||||
// The runs share one creation time, so the order is not defined. Sort by status.
|
||||
response.body.data.sort((a: { status: string }, b: { status: string }) =>
|
||||
a.status.localeCompare(b.status),
|
||||
);
|
||||
expect(response.body).toStrictEqual({
|
||||
data: [
|
||||
{
|
||||
id: completedRun.id,
|
||||
status: 'completed',
|
||||
runAt: null,
|
||||
completedAt: null,
|
||||
metrics: {},
|
||||
errorCode: null,
|
||||
errorDetails: null,
|
||||
finalResult: 'success',
|
||||
testCaseCount: 0,
|
||||
createdAt: completedRun.createdAt.toISOString(),
|
||||
updatedAt: completedRun.updatedAt.toISOString(),
|
||||
},
|
||||
{
|
||||
id: errorRun.id,
|
||||
status: 'error',
|
||||
runAt: null,
|
||||
completedAt: null,
|
||||
metrics: {},
|
||||
errorCode: null,
|
||||
errorDetails: null,
|
||||
finalResult: null,
|
||||
testCaseCount: 0,
|
||||
createdAt: errorRun.createdAt.toISOString(),
|
||||
updatedAt: errorRun.updatedAt.toISOString(),
|
||||
},
|
||||
],
|
||||
nextCursor: null,
|
||||
});
|
||||
});
|
||||
|
||||
test('GET /workflows/:workflowId/versions/:workflowVersionId returns the full version', async () => {
|
||||
const [version] = target.versions;
|
||||
|
||||
const response = await get(`/workflows/${target.workflow.id}/versions/${version.versionId}`);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body).toStrictEqual({
|
||||
versionId: version.versionId,
|
||||
workflowId: target.workflow.id,
|
||||
nodes: version.nodes,
|
||||
connections: version.connections,
|
||||
nodeGroups: [],
|
||||
authors: 'Test Author',
|
||||
name: 'Target v1',
|
||||
description: 'Target v1 description',
|
||||
createdAt: version.createdAt.toISOString(),
|
||||
updatedAt: version.updatedAt.toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
test('GET /workflows/:workflowId/:workflowVersionId returns the same version with a Deprecation header', async () => {
|
||||
const [, version] = target.versions;
|
||||
|
||||
const alias = await get(`/workflows/${target.workflow.id}/${version.versionId}`);
|
||||
const canonical = await get(`/workflows/${target.workflow.id}/versions/${version.versionId}`);
|
||||
|
||||
expect(alias.statusCode).toBe(200);
|
||||
expect(alias.headers.deprecation).toMatch(/^@\d+$/);
|
||||
expect(alias.body).toStrictEqual({
|
||||
versionId: version.versionId,
|
||||
workflowId: target.workflow.id,
|
||||
nodes: version.nodes,
|
||||
connections: version.connections,
|
||||
nodeGroups: [],
|
||||
authors: 'Test Author',
|
||||
name: 'Target v2',
|
||||
description: 'Target v2 description',
|
||||
createdAt: version.createdAt.toISOString(),
|
||||
updatedAt: version.updatedAt.toISOString(),
|
||||
});
|
||||
expect(alias.body).toStrictEqual(canonical.body);
|
||||
});
|
||||
});
|
||||
|
|
@ -1073,6 +1073,7 @@ describe('GET /workflows/:id/:versionId', () => {
|
|||
const response = await authOwnerAgent.get('/workflows/non-existing/version-123');
|
||||
|
||||
expect(response.statusCode).toBe(404);
|
||||
expect(response.headers.deprecation).toBe('@1787702400');
|
||||
// The deprecated path keeps one message for both cases. Callers may match on it.
|
||||
expect(response.body.message).toBe('Version not found');
|
||||
});
|
||||
|
|
@ -1110,6 +1111,9 @@ describe('GET /workflows/:id/:versionId', () => {
|
|||
|
||||
const response = await authOwnerAgent.get(`/workflows/${workflow.id}/${versionId}`);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.headers.deprecation).toBe('@1787702400');
|
||||
|
||||
const body = response.body as Partial<WorkflowHistory>;
|
||||
expect(body).toEqual({
|
||||
workflowId: workflow.id,
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@
|
|||
"status": "covered",
|
||||
"nodeOperation": "workflow:update"
|
||||
},
|
||||
"GET /workflows/{id}/{versionId}": {
|
||||
"GET /workflows/{workflowId}/{workflowVersionId}": {
|
||||
"status": "covered",
|
||||
"nodeOperation": "workflow:getVersion"
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user