mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-22 04:21:32 +02:00
refactor(core): Remove the catalog runs endpoint
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) <noreply@anthropic.com>
This commit is contained in:
parent
4923503526
commit
af797b24d1
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<typeof mock<Logger>>;
|
||||
let finder: ReturnType<typeof mock<WorkflowFinderService>>;
|
||||
let schemas: ReturnType<typeof mock<WorkflowInputSchemaService>>;
|
||||
let executions: ReturnType<typeof mock<ExecutionService>>;
|
||||
|
||||
beforeEach(() => {
|
||||
logger = mock<Logger>();
|
||||
finder = mock<WorkflowFinderService>();
|
||||
schemas = mock<WorkflowInputSchemaService>();
|
||||
executions = mock<ExecutionService>();
|
||||
|
||||
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 }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<WorkflowParam>, _res: unknown, @Body payload: CatalogRunDto) {
|
||||
|
|
|
|||
|
|
@ -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<CatalogRunListing> {
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user