From af797b24d194dc21958da03e3e59fe4e9cff08ed Mon Sep 17 00:00:00 2001 From: Yuliia Pominchuk Date: Wed, 5 Aug 2026 13:10:24 +0200 Subject: [PATCH] refactor(core): Remove the catalog runs endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nothing calls it: the catalog now sends people to the workflow's own execution list, which shows the same runs with their data instead of a name, a time and a status. Takes `CatalogService.listRuns`, its limit and its response types with it, and drops the `ExecutionService` dependency that existed only for it. The `catalogRunUserId` metadata the run service writes stays. It is the only record of which person a run belongs to — a scheduled run has no session behind it, so nothing else on the execution says whose it is — and unlike a query, it cannot be reconstructed once it is not written. Co-Authored-By: Claude Opus 5 (1M context) --- docs/workflow-catalog-handoff.md | 4 +- .../__tests__/catalog.service.test.ts | 73 +------------------ .../cli/src/workflows/catalog.controller.ts | 10 --- packages/cli/src/workflows/catalog.service.ts | 56 +------------- 4 files changed, 5 insertions(+), 138 deletions(-) diff --git a/docs/workflow-catalog-handoff.md b/docs/workflow-catalog-handoff.md index f7ad04f70ce..420af0dbce0 100644 --- a/docs/workflow-catalog-handoff.md +++ b/docs/workflow-catalog-handoff.md @@ -68,7 +68,7 @@ explicitly in the service before deleting rows. | `0a36cf18a7d` | Input-contract reader, extracted from `nodes-base` into `n8n-workflow` | | `f78b151d7f2` | `POST /catalog/workflows/:id/run` | | `1c7cde1ae96` | `GET /catalog/workflows` | -| `32fb2405e58` | `GET /catalog/runs` | +| `32fb2405e58` | `GET /catalog/runs` — since removed, superseded by the workflow's own execution list | | `d400001ec62` | Frontend `/catalog` tab | | _(uncommitted)_ | Binding + subscription tables, scheduler owner scope, schedule UI | @@ -89,7 +89,7 @@ explicitly in the service before deleting rows. - `WorkflowInputSchemaService` — answers eligibility and schema together - `CatalogRunService` — start node, production mode, input filtering, execution tagging -- `CatalogService` — listing + own-runs history +- `CatalogService` — listing - `CatalogController` at `/rest/catalog` ### Frontend diff --git a/packages/cli/src/workflows/__tests__/catalog.service.test.ts b/packages/cli/src/workflows/__tests__/catalog.service.test.ts index 6ddfe52049d..75281f622e5 100644 --- a/packages/cli/src/workflows/__tests__/catalog.service.test.ts +++ b/packages/cli/src/workflows/__tests__/catalog.service.test.ts @@ -2,13 +2,7 @@ import type { Logger } from '@n8n/backend-common'; import type { User } from '@n8n/db'; import { mock } from 'vitest-mock-extended'; -import type { ExecutionService } from '@/executions/execution.service'; -import { CATALOG_RUN_USER_KEY } from '@/workflows/catalog-run.service'; -import { - CATALOG_LIST_LIMIT, - CATALOG_RUNS_LIMIT, - CatalogService, -} from '@/workflows/catalog.service'; +import { CATALOG_LIST_LIMIT, CatalogService } from '@/workflows/catalog.service'; import type { WorkflowFinderService } from '@/workflows/workflow-finder.service'; import type { WorkflowInputSchemaService } from '@/workflows/workflow-input-schema.service'; @@ -25,22 +19,19 @@ describe('CatalogService', () => { let logger: ReturnType>; let finder: ReturnType>; let schemas: ReturnType>; - let executions: ReturnType>; beforeEach(() => { logger = mock(); finder = mock(); schemas = mock(); - executions = mock(); schemas.describe.mockResolvedValue({ eligible: true, trigger: 'manual-trigger', fields: [], }); - executions.findRangeWithCount.mockResolvedValue({ results: [], count: 0, estimated: false }); - service = new CatalogService(logger, finder, schemas, executions); + service = new CatalogService(logger, finder, schemas); }); it('should list only workflows the user may execute', async () => { @@ -110,64 +101,4 @@ describe('CatalogService', () => { expect(result.workflows).toHaveLength(CATALOG_LIST_LIMIT); expect(logger.warn).toHaveBeenCalled(); }); - - describe('listRuns', () => { - it('should select only runs the person started themselves', async () => { - await service.listRuns(user); - - expect(executions.findRangeWithCount).toHaveBeenCalledWith( - expect.objectContaining({ - user, - metadata: [{ key: CATALOG_RUN_USER_KEY, value: 'user-1', exactMatch: true }], - range: { limit: CATALOG_RUNS_LIMIT }, - order: { startedAt: 'DESC' }, - }), - ); - }); - - it('should scope visibility to workflows the person may execute', async () => { - executions.buildSharingOptions.mockResolvedValue({ scopes: ['workflow:execute'] }); - - await service.listRuns(user); - - expect(executions.buildSharingOptions).toHaveBeenCalledWith('workflow:execute'); - expect(executions.findRangeWithCount).toHaveBeenCalledWith( - expect.objectContaining({ sharingOptions: { scopes: ['workflow:execute'] } }), - ); - }); - - it('should not expose node names through the run summary', async () => { - executions.findRangeWithCount.mockResolvedValue({ - results: [ - { - id: 'exec-1', - workflowId: 'wf-1', - workflowName: 'Weekly report', - status: 'success', - startedAt: new Date('2026-01-01'), - stoppedAt: new Date('2026-01-02'), - mode: 'trigger', - createdAt: new Date('2026-01-01'), - lastNodeExecuted: 'Send Email', - }, - ], - count: 1, - estimated: false, - }); - - const result = await service.listRuns(user); - - expect(result.runs).toEqual([ - { - id: 'exec-1', - workflowId: 'wf-1', - workflowName: 'Weekly report', - status: 'success', - startedAt: new Date('2026-01-01'), - stoppedAt: new Date('2026-01-02'), - }, - ]); - expect(result).toEqual(expect.objectContaining({ count: 1, estimated: false })); - }); - }); }); diff --git a/packages/cli/src/workflows/catalog.controller.ts b/packages/cli/src/workflows/catalog.controller.ts index 141c845ad38..3920ff83804 100644 --- a/packages/cli/src/workflows/catalog.controller.ts +++ b/packages/cli/src/workflows/catalog.controller.ts @@ -48,16 +48,6 @@ export class CatalogController { return await this.catalogService.list(req.user); } - /** - * Scoped to executions the person started themselves, by the marker the run - * service writes — not to the workflows they can reach. - */ - @Get('/runs') - @GlobalScope('workflow:list') - async listRuns(req: AuthenticatedRequest) { - return await this.catalogService.listRuns(req.user); - } - @Post('/workflows/:workflowId/run') @ProjectScope('workflow:execute') async run(req: AuthenticatedRequest, _res: unknown, @Body payload: CatalogRunDto) { diff --git a/packages/cli/src/workflows/catalog.service.ts b/packages/cli/src/workflows/catalog.service.ts index 89e4c1c7877..65786477977 100644 --- a/packages/cli/src/workflows/catalog.service.ts +++ b/packages/cli/src/workflows/catalog.service.ts @@ -1,10 +1,8 @@ import { Logger } from '@n8n/backend-common'; import type { User } from '@n8n/db'; import { Service } from '@n8n/di'; -import type { ExecutionStatus, FieldValueOption } from 'n8n-workflow'; +import type { FieldValueOption } from 'n8n-workflow'; -import { ExecutionService } from '@/executions/execution.service'; -import { CATALOG_RUN_USER_KEY } from '@/workflows/catalog-run.service'; import { WorkflowFinderService } from '@/workflows/workflow-finder.service'; import type { StartTrigger } from '@/workflows/workflow-input-schema.service'; import { WorkflowInputSchemaService } from '@/workflows/workflow-input-schema.service'; @@ -35,31 +33,12 @@ export type CatalogListing = { truncated: boolean; }; -/** How many past runs one page of history returns. */ -export const CATALOG_RUNS_LIMIT = 50; - -export type CatalogRun = { - id: string; - workflowId: string; - workflowName?: string; - status: ExecutionStatus; - startedAt: Date | null; - stoppedAt?: Date; -}; - -export type CatalogRunListing = { - runs: CatalogRun[]; - count: number; - estimated: boolean; -}; - @Service() export class CatalogService { constructor( private readonly logger: Logger, private readonly workflowFinderService: WorkflowFinderService, private readonly workflowInputSchemaService: WorkflowInputSchemaService, - private readonly executionService: ExecutionService, ) {} /** @@ -111,37 +90,4 @@ export class CatalogService { truncated, }; } - - /** - * A person's own catalog runs, most recent first. - * - * Scoped by the run marker rather than by workflow, so someone sharing a - * workflow with colleagues sees only what they themselves started. - */ - async listRuns(user: User): Promise { - const sharingOptions = await this.executionService.buildSharingOptions('workflow:execute'); - - const { results, count, estimated } = await this.executionService.findRangeWithCount({ - kind: 'range', - user, - sharingOptions, - metadata: [{ key: CATALOG_RUN_USER_KEY, value: user.id, exactMatch: true }], - range: { limit: CATALOG_RUNS_LIMIT }, - order: { startedAt: 'DESC' }, - }); - - // Projected down from the execution summary on purpose: it also carries - // `lastNodeExecuted`, per-node statuses and error details, which would - // hand node names to someone who cannot read the workflow. - const runs = results.map(({ id, workflowId, workflowName, status, startedAt, stoppedAt }) => ({ - id, - workflowId, - workflowName, - status, - startedAt, - stoppedAt, - })); - - return { runs, count, estimated }; - } }