mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-29 19:10:43 +08:00
fix(core): Beta import/export API not available on cloud (no-changelog) (#33382)
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
cubic-dev-ai[bot]
parent
613b2c5acd
commit
3ead311851
@@ -2,10 +2,7 @@
|
||||
|
||||
Export and import workflows as portable n8n packages (`.n8np` archives).
|
||||
|
||||
> **Beta — disabled by default.** These commands call beta endpoints that the
|
||||
> target instance must opt into with `N8N_PUBLIC_API_PACKAGES_ENABLED=true`, and
|
||||
> the n8n Packages feature must be licensed. While disabled, the instance returns
|
||||
> `404` and the CLI prints a hint explaining how to enable it.
|
||||
> **Beta feature:** n8n packages are still under development and there may be breaking changes on APIs.
|
||||
|
||||
## `package export`
|
||||
|
||||
|
||||
@@ -9,14 +9,6 @@ describe('toPackagesError', () => {
|
||||
expect(toPackagesError(error)).toBe(error);
|
||||
});
|
||||
|
||||
it('adds a beta-enablement hint for a 404', () => {
|
||||
const result = toPackagesError(new ApiError(404, 'Not Found'));
|
||||
|
||||
expect(result).toBeInstanceOf(ApiError);
|
||||
expect((result as ApiError).statusCode).toBe(404);
|
||||
expect((result as ApiError).hint).toContain('N8N_PUBLIC_API_PACKAGES_ENABLED=true');
|
||||
});
|
||||
|
||||
it('lists workflow-conflict issues for a 409', () => {
|
||||
const result = toPackagesError(
|
||||
new ApiError(409, 'Import blocked', undefined, {
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { ApiError } from '../../client';
|
||||
|
||||
const BETA_HINT =
|
||||
'The n8n Packages API is beta and disabled by default. Enable it on the instance with ' +
|
||||
'N8N_PUBLIC_API_PACKAGES_ENABLED=true (requires the n8n Packages license).';
|
||||
|
||||
type BlockingIssue =
|
||||
| {
|
||||
type: 'workflow-conflict';
|
||||
@@ -35,9 +31,6 @@ function issuesHint(details: unknown): string | undefined {
|
||||
|
||||
export function toPackagesError(error: unknown): unknown {
|
||||
if (!(error instanceof ApiError)) return error;
|
||||
if (error.statusCode === 404) {
|
||||
return new ApiError(error.statusCode, error.message, BETA_HINT, error.details);
|
||||
}
|
||||
const hint = issuesHint(error.details);
|
||||
if (hint) {
|
||||
return new ApiError(error.statusCode, error.message, hint, error.details);
|
||||
|
||||
@@ -13,11 +13,4 @@ export class PublicApiConfig {
|
||||
/** When true, the Swagger UI for the Public API is not served. */
|
||||
@Env('N8N_PUBLIC_API_SWAGGERUI_DISABLED')
|
||||
swaggerUiDisabled: boolean = false;
|
||||
|
||||
/**
|
||||
* When true, the n8n-packages public API endpoints are enabled. This feature is
|
||||
* currently in beta and disabled by default.
|
||||
*/
|
||||
@Env('N8N_PUBLIC_API_PACKAGES_ENABLED')
|
||||
packagesEnabled: boolean = false;
|
||||
}
|
||||
|
||||
@@ -194,7 +194,6 @@ describe('GlobalConfig', () => {
|
||||
disabled: false,
|
||||
path: 'api',
|
||||
swaggerUiDisabled: false,
|
||||
packagesEnabled: false,
|
||||
},
|
||||
templates: {
|
||||
enabled: true,
|
||||
|
||||
@@ -64,9 +64,3 @@ The in-tree addition of `workflowIdPolicy` is the canonical example — it lande
|
||||
in the module types + importer, the DTO (+ form-fields list), the handler, and
|
||||
the CLI. Grep `workflowIdPolicy` to see every site a new import knob must touch.
|
||||
|
||||
## More context
|
||||
|
||||
See the module structure and import-pipeline flow notes in the root
|
||||
`AGENTS.md` and the package import/export RFC. Licensed behind
|
||||
`feat:n8nPackages`; the public-API endpoints are beta and gated by
|
||||
`N8N_PUBLIC_API_PACKAGES_ENABLED`.
|
||||
|
||||
-19
@@ -1,5 +1,4 @@
|
||||
import { mockInstance } from '@n8n/backend-test-utils';
|
||||
import { GlobalConfig } from '@n8n/config';
|
||||
import type { AuthenticatedRequest } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import type { Response } from 'express';
|
||||
@@ -8,7 +7,6 @@ import type { Mocked } from 'vitest';
|
||||
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
|
||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||
import { N8nPackagesService } from '@/modules/n8n-packages/n8n-packages.service';
|
||||
import * as middlewares from '@/public-api/v1/shared/middlewares/global.middleware';
|
||||
|
||||
@@ -30,7 +28,6 @@ beforeAll(async () => {
|
||||
});
|
||||
|
||||
describe('n8n-packages handler', () => {
|
||||
let packagesEnabled: boolean;
|
||||
let mockService: Mocked<N8nPackagesService>;
|
||||
|
||||
function makeRequest(
|
||||
@@ -62,13 +59,9 @@ describe('n8n-packages handler', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
packagesEnabled = true;
|
||||
mockService = mockInstance(N8nPackagesService);
|
||||
|
||||
vi.spyOn(Container, 'get').mockImplementation((serviceClass) => {
|
||||
if (serviceClass === GlobalConfig) {
|
||||
return { publicApi: { packagesEnabled } } as never;
|
||||
}
|
||||
if (
|
||||
serviceClass === N8nPackagesService ||
|
||||
(typeof serviceClass === 'function' && serviceClass.name === 'N8nPackagesService')
|
||||
@@ -79,18 +72,6 @@ describe('n8n-packages handler', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('throws NotFoundError when packages are disabled', async () => {
|
||||
packagesEnabled = false;
|
||||
|
||||
const caught = await run(
|
||||
makeRequest({ workflowIds: ['wf-1'] }, ['workflow:export']),
|
||||
makeResponse(),
|
||||
);
|
||||
|
||||
expect(caught).toBeInstanceOf(NotFoundError);
|
||||
expect(mockService.exportPackage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws BadRequestError when the payload fails DTO validation', async () => {
|
||||
const caught = await run(
|
||||
makeRequest({ workflowIds: [''] }, ['workflow:export']),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ExportPackageRequestDto, ImportPackageRequestDto } from '@n8n/api-types';
|
||||
import { GlobalConfig } from '@n8n/config';
|
||||
import type { AuthenticatedRequest } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import type { ApiKeyScope } from '@n8n/permissions';
|
||||
@@ -15,7 +14,6 @@ import {
|
||||
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
|
||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||
import { N8nPackagesService } from '@/modules/n8n-packages/n8n-packages.service';
|
||||
import { resolveImportPackageUpload } from '@/modules/n8n-packages/utils/import-package-upload';
|
||||
|
||||
@@ -80,10 +78,6 @@ const n8nPackagesHandlers: N8nPackagesHandlers = {
|
||||
exportPackage: [
|
||||
publicApiCompositeScope(PACKAGE_EXPORT_SCOPES),
|
||||
async (req, res) => {
|
||||
if (!Container.get(GlobalConfig).publicApi.packagesEnabled) {
|
||||
throw new NotFoundError('Not Found');
|
||||
}
|
||||
|
||||
const payload = ExportPackageRequestDto.safeParse(req.body);
|
||||
if (!payload.success) {
|
||||
throw new BadRequestError(payload.error.errors.map(({ message }) => message).join('; '));
|
||||
@@ -113,10 +107,6 @@ const n8nPackagesHandlers: N8nPackagesHandlers = {
|
||||
importPackage: [
|
||||
publicApiScope('workflow:import'),
|
||||
async (req, res) => {
|
||||
if (!Container.get(GlobalConfig).publicApi.packagesEnabled) {
|
||||
throw new NotFoundError('Not Found');
|
||||
}
|
||||
|
||||
const packageFile = resolveImportPackageUpload(req);
|
||||
|
||||
const payload = ImportPackageRequestDto.safeParse(req.body ?? {});
|
||||
|
||||
+1
-2
@@ -6,8 +6,7 @@ post:
|
||||
- N8nPackage
|
||||
summary: 'Beta: Export workflows and projects as an n8n package'
|
||||
description: |
|
||||
**Beta — disabled by default.** Set `N8N_PUBLIC_API_PACKAGES_ENABLED=true` on the
|
||||
instance to enable this endpoint. While disabled, requests return `404`.
|
||||
**Beta** — breaking changes may still occur without major version bump.
|
||||
|
||||
Export either workflows or projects as a gzipped tar archive (.n8np).
|
||||
Provide `workflowIds` or `projectIds`, but not both. Empty projects export
|
||||
|
||||
+1
-2
@@ -6,8 +6,7 @@ post:
|
||||
- N8nPackage
|
||||
summary: 'Beta: Import an n8n package into a project'
|
||||
description: |
|
||||
**Beta — disabled by default.** Set `N8N_PUBLIC_API_PACKAGES_ENABLED=true` on the
|
||||
instance to enable this endpoint. While disabled, requests return `404`.
|
||||
**Beta** — breaking changes may still occur without major version bump.
|
||||
|
||||
Imports a gzip-compressed tar package (`.n8np`) into the target project. Send the
|
||||
archive as the multipart field `package`. Optional routing uses form fields
|
||||
|
||||
@@ -45,7 +45,7 @@ tags:
|
||||
- name: Folders
|
||||
description: Operations about folders
|
||||
- name: N8nPackage
|
||||
description: Beta — operations about n8n packages. Disabled by default; enable with `N8N_PUBLIC_API_PACKAGES_ENABLED=true`.
|
||||
description: Beta — breaking changes may still occur without major version bump.
|
||||
|
||||
paths:
|
||||
/audit:
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { testDb } from '@n8n/backend-test-utils';
|
||||
import { GlobalConfig } from '@n8n/config';
|
||||
import type { User } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { InstanceSettings } from 'n8n-core';
|
||||
@@ -20,11 +19,9 @@ beforeAll(async () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
authOwnerAgent = testServer.publicApiAgentFor(owner);
|
||||
Container.get(GlobalConfig).publicApi.packagesEnabled = true;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
Container.get(GlobalConfig).publicApi.packagesEnabled = false;
|
||||
await testDb.truncate(['WorkflowEntity', 'SharedWorkflow', 'ProjectRelation', 'Project']);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { mockInstance, testDb } from '@n8n/backend-test-utils';
|
||||
import { GlobalConfig } from '@n8n/config';
|
||||
import type { Project, User } from '@n8n/db';
|
||||
import { ProjectRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
@@ -44,11 +43,6 @@ beforeEach(async () => {
|
||||
'SharedCredentials',
|
||||
]);
|
||||
authOwnerAgent = testServer.publicApiAgentFor(owner);
|
||||
Container.get(GlobalConfig).publicApi.packagesEnabled = true;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Container.get(GlobalConfig).publicApi.packagesEnabled = false;
|
||||
});
|
||||
|
||||
const testWithAPIKey = (method: 'post', url: string, apiKey: string | null) => async () => {
|
||||
|
||||
Reference in New Issue
Block a user